Getting Started
Install commiq, build a store with typed commands, and wire it into React.
Getting Started
Upgrading from 1.x? Read the migration guide — state reads, queue() and handler subscriptions all changed.
Installation
# Core library
pnpm add @naikidev/commiq
# React bindings (optional)
pnpm add @naikidev/commiq-react# Core library
npm install @naikidev/commiq
# React bindings (optional)
npm install @naikidev/commiq-react# Core library
yarn add @naikidev/commiq
# React bindings (optional)
yarn add @naikidev/commiq-react# Core library
bun add @naikidev/commiq
# React bindings (optional)
bun add @naikidev/commiq-reactYour first store
1. Define the state and the commands
A CommandDef carries a command's name and its payload type together, so a wrong payload is a compile error rather than a runtime surprise.
import { createCommandDef } from "@naikidev/commiq";
export type CounterState = {
count: number;
};
export const increment = createCommandDef("increment");
export const addAmount = createCommandDef<number>("addAmount");2. Create the store and register handlers
import { createStore } from "@naikidev/commiq";
const store = createStore<CounterState>({ count: 0 }, {
onError: (report) => console.error(`[${report.source}]`, report.error),
});
store
.addCommandHandler(increment, (ctx) => {
ctx.setState({ count: ctx.state.count + 1 });
})
.addCommandHandler(addAmount, (ctx, cmd) => {
ctx.setState({ count: ctx.state.count + cmd.data });
});ctx.state is DeepReadonly<CounterState> — build a new state object instead of writing to it. Outside production the state is deep-frozen, so an accidental write throws.
onError is optional; it defaults to console.error outside production. Wire it to your error reporter in real applications, because a handler that throws does not propagate out of queue().
3. Seal the store
sealStore returns a facade with no registration methods, so consumers can only read state, dispatch commands and observe events.
import { sealStore } from "@naikidev/commiq";
export const counterStore = sealStore(store);Sealing is a boundary, not a sandbox. state is DeepReadonly and frozen in development, so a stray write is a type error and throws at runtime. In a production build there is no freeze, and Map, Set and class instances in state are never frozen — see what sealing guarantees.
4. Use it
Commands are queued and processed asynchronously. State does not change on the tick you dispatch:
counterStore.queue(increment);
counterStore.state.count; // still 0 — the queue drains on a microtask
await counterStore.flush();
counterStore.state.count; // 1flush() waits for the whole store to go quiet, including commands queued by event handlers. To wait for one command and learn whether it succeeded, await the handle queue() returns:
const result = await counterStore.queue(addAmount, 5);
result.status; // "handled" | "failed" | "interrupted" | "invalid" | "discarded"
counterStore.state.count; // 6A command handle never rejects, so ignoring it is safe and try/catch around it catches nothing. Check result.status, or supply onError.
Using with React
Hooks take the store directly — no provider needed.
import { useSelector, useQueue } from "@naikidev/commiq-react";
import { counterStore, increment } from "./stores/counter";
function Counter() {
const count = useSelector(counterStore, (s) => s.count);
const dispatch = useQueue(counterStore);
const handleIncrement = () => dispatch(increment);
return (
<div>
<p>{count}</p>
<button onClick={handleIncrement}>+1</button>
</div>
);
}useSelector re-renders only when the selected value changes. Extract callbacks into named handle* functions rather than inlining them in JSX props.
CommiqProvider is optional and only needed when a store name must resolve to a different instance per subtree or per request — SSR and tests. See React hooks.
Add devtools
During development, drop in CommiqDevtools to see the event timeline, trace causality chains, inspect state and dispatch commands by hand.
pnpm add @naikidev/commiq-devtoolsnpm install @naikidev/commiq-devtoolsyarn add @naikidev/commiq-devtoolsbun add @naikidev/commiq-devtoolsimport { CommiqDevtools } from "@naikidev/commiq-devtools";
import { counterStore } from "./stores/counter";
function App() {
return (
<>
<Counter />
<CommiqDevtools stores={{ counter: counterStore }} />
</>
);
}The panel renders nothing when NODE_ENV is production; pass enabled to override that. See devtools React for the panel, and devtools for the framework-agnostic instrumentation engine underneath it.
Next steps
- Store API — handlers, the queue, suspension, lifecycle and the builtin events
- Commands and events — definitions, event identity, sealing and the event bus
- React hooks — the full hook surface
- Patterns — how to structure real applications