Emittor is a tiny, evented state holder. It lets you share state across React components without context providers. It ships with:
- State + pub/sub core
- React hook
useEmittor - Pluggable middlewares via
.use(cb)and.use(factory, true) - Lifecycle:
onInit,init(),onDeinit,deinit() - Optional reducer factory to attach named actions to
em.reducers
Bring batteries with the companion package emittor-middlewares. For advanced patterns (URL params, localStorage sync, debounce), see that README.
npm i emittor
# or
yarn add emittor
# or
pnpm add emittorCreate a dedicated module for your emittor. Do not create it inside a component.
// lib/counter.ts
import { createEmittor } from "emittor";
export const counter = createEmittor(0);Use it in any client component.
"use client";
import { useEmittor } from "emittor";
import { counter } from "../lib/counter";
export default function Counter() {
const [count, setCount] = useEmittor(counter);
return (
<div>
<button onClick={() => setCount((c) => c - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}Note: useEmittor calls emittor.init() once on mount.
Use .use() to add middlewares. Factories run once and return a middleware. Keep side effects inside the middleware and call next() to continue.
- Need URL syncing, localStorage, or debounce? See
emittor-middlewares. - For custom middlewares, use this signature:
(next, em) => { /* side effect */; next(); }.
// example: simple logger
counter.use((next, em) => {
console.log("[counter]", em.getState());
next();
});Attach named actions once using a reducer factory. They live on em.reducers.
// lib/counter.ts
import { createEmittor } from "emittor";
export const counter = createEmittor(0, {
reducerFactory: (em) => ({
increment(by: number = 1) { em.setState(em.state + by); },
decrement(by: number = 1) { em.setState(em.state - by); },
reset() { em.setState(em.defaultState); },
}),
});Use in components:
"use client";
import { useEmittor } from "emittor";
import { counter } from "../lib/counter";
export default function ActionsExample() {
const [count] = useEmittor(counter);
return (
<div>
<div>{count}</div>
<button onClick={() => counter.reducers.increment()}>+</button>
<button onClick={() => counter.reducers.decrement()}>-</button>
<button onClick={() => counter.reducers.reset()}>reset</button>
</div>
);
}Register one-time setup in onInit. Return a cleanup function if needed. useEmittor calls em.init() for you.
// lib/theme.ts
import { createEmittor } from "emittor";
export const theme = createEmittor<"light" | "dark">("light");
theme.onInit((em) => {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const apply = () => em.setState(mq.matches ? "dark" : "light");
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
});Any component using useEmittor(em) will update when em.setState is called anywhere.
import { useEmittor } from "emittor";
import { counter } from "../lib/counter";
function View() {
const [count] = useEmittor(counter);
return <div>{count}</div>;
}
function Button() {
return <button onClick={() => counter.setState(counter.state + 1)}>Add</button>;
}counter.getState();
counter.setState(10);
counter.state; // getter
counter.state = 10; // setter (calls setState)// useCreateEmittor: returns { state, emittor }
import { useCreateEmittor } from "emittor";
function LocalCounter() {
const { state, emittor } = useCreateEmittor(0);
return <button onClick={() => emittor.setState(state + 1)}>{state}</button>;
}// useMotionEmittor: create and memoize an emittor (no subscription)
import { useMotionEmittor } from "emittor";
function Maker() {
const em = useMotionEmittor({ open: false });
return null;
}- Create each emittor in its own module. Do not create inside a component.
- Use
"use client"on components that calluseEmittor. useEmittorcallsem.init()on the client. Safe for Next.js App Router.
Properties
state: Tgetter/setterdefaultState: Tinitial statereducers: Ractions fromreducerFactory(optional)emitalias ofsetStaterefreshalias ofexec
Constructor
new Emittor(initialState: T, options?: { match?: boolean; reducerFactory?: (em) => R })match(default true): skip updates whennext === current
Core
setState(state: T): voidgetState(): Trun(state: T): voidruns middleware chain then subscribers with provided state (does not change state)exec(): voidruns middleware/subscribers with current state
Subscriptions
connect(cb: (state: T) => void): () => voiddisconnect(cb): void
Middleware
use(cb: Middleware<T, R>): thisuse(factory: MiddlewareFactory<T, R>, true): this
Lifecycle
onInit((em) => void | () => void): thisinit(force = false): voidonDeinit(cb: () => void): voiddeinit(): void
For URL param sync, localStorage sync, and debounced side effects, see:
emittor-middlewareson npm- The usage examples in its README
MIT