Store Machine
What is a Store Machine?
Section titled “What is a Store Machine?”A Store Machine is a lightweight, event-driven store for managing a single value with type-safe actions. Unlike Factory Machines which manage discrete states with transitions, Store Machines manage a continuous value that can be updated through dispatched actions.
Use Store Machine when:
- You have a single value (object, number, array) that changes over time
- You want Redux-like action dispatch semantics
- You need lifecycle hooks for side effects
- You want type-safe action parameters
Use Factory Machine when:
- You have distinct states (Idle, Loading, Success, Error)
- State transitions follow specific rules
- You need pattern matching on state
Creating a Store Machine
Section titled “Creating a Store Machine”import { createStoreMachine } from "matchina";
const counter = createStoreMachine(0, { increment: (amt = 1) => (change) => change.from + amt, decrement: (amt = 1) => (change) => change.from - amt, set: (value: number) => value, reset: () => 0,});The first argument is the initial value, and the second is a record of actions.
Action Types
Section titled “Action Types”Actions can return values in two ways:
Direct Return
Section titled “Direct Return”Return the new value directly:
set: (value: number) => value,reset: () => 0,Curried Return
Section titled “Curried Return”Return a function that receives the change context:
increment: (amt = 1) => (change) => change.from + amt,toggle: () => (change) => !change.from,The change object contains:
from- The previous valueto- The next value (set after your function returns)type- The action nameparams- The parameters passed to dispatch
Dispatching Actions
Section titled “Dispatching Actions”counter.dispatch("increment"); // Uses default parametercounter.dispatch("increment", 5); // Explicit parametercounter.dispatch("set", 42); // Required parametercounter.dispatch("reset"); // No parametersGetting State
Section titled “Getting State”const value = counter.getState(); // Current valueconst change = counter.getChange(); // Last change eventInteractive Example
Section titled “Interactive Example”import { createStoreMachine } from "matchina";
export function createCounterStore(initialValue = 0) { return createStoreMachine(initialValue, { increment: (amt = 1) => (change) => change.from + amt, decrement: (amt = 1) => (change) => change.from - amt, set: (value: number) => value, reset: () => 0, });}
export type CounterStore = ReturnType<typeof createCounterStore>;import { useSyncExternalStore } from "react";import type { CounterStore } from "./store";
export function StoreCounterView({ store }: { store: CounterStore }) { const count = useSyncExternalStore( (callback) => { const orig = store.notify; store.notify = (ev) => { orig.call(store, ev); callback(); }; return () => { store.notify = orig; }; }, () => store.getState() );
return ( <div className="flex flex-col items-center gap-4 p-6"> <div className="text-6xl font-bold tabular-nums">{count}</div>
<div className="flex gap-2"> <button onClick={() => store.dispatch("decrement")} className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors" > -1 </button> <button onClick={() => store.dispatch("increment")} className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors" > +1 </button> </div>
<div className="flex gap-2"> <button onClick={() => store.dispatch("decrement", 5)} className="px-3 py-1 bg-red-400 text-white rounded text-sm hover:bg-red-500 transition-colors" > -5 </button> <button onClick={() => store.dispatch("increment", 5)} className="px-3 py-1 bg-green-400 text-white rounded text-sm hover:bg-green-500 transition-colors" > +5 </button> </div>
<div className="flex gap-2 mt-2"> <button onClick={() => store.dispatch("set", 100)} className="px-3 py-1 bg-blue-500 text-white rounded text-sm hover:bg-blue-600 transition-colors" > Set 100 </button> <button onClick={() => store.dispatch("reset")} className="px-3 py-1 bg-gray-500 text-white rounded text-sm hover:bg-gray-600 transition-colors" > Reset </button> </div> </div> );}Using with Immer
Section titled “Using with Immer”For complex nested state, combine with Immer for immutable updates:
import { produce } from "immer";import { createStoreMachine } from "matchina";
const mutate = <T>(fn: (draft: T) => void) => (change: { from: T }) => produce(change.from, fn);
type AppState = { user: { name: string }; todos: { id: string; text: string; done: boolean }[];};
const store = createStoreMachine<AppState>(initialState, { setUserName: (name: string) => mutate(draft => { draft.user.name = name; }),
addTodo: (text: string) => mutate(draft => { draft.todos.push({ id: Date.now().toString(), text, done: false }); }),
toggleTodo: (id: string) => mutate(draft => { const todo = draft.todos.find(t => t.id === id); if (todo) todo.done = !todo.done; }),});Lifecycle Hooks
Section titled “Lifecycle Hooks”Store Machines support the same lifecycle hooks as Factory Machines:
import { setup, effect, when } from "matchina";
setup(store)( effect( when( (ev) => ev.type === "addTodo", (ev) => { console.log(`Added todo: ${ev.params[0]}`); } ) ),
effect((ev) => { // Runs on every change localStorage.setItem("state", JSON.stringify(ev.to)); }));React Integration
Section titled “React Integration”import { useSyncExternalStore } from "react";
function Counter({ store }) { const count = useSyncExternalStore( (callback) => { const orig = store.notify; store.notify = (ev) => { orig.call(store, ev); callback(); }; return () => { store.notify = orig; }; }, () => store.getState() );
return ( <div> <span>{count}</span> <button onClick={() => store.dispatch("increment")}>+</button> <button onClick={() => store.dispatch("decrement")}>-</button> </div> );}Comparison: Store Machine vs Factory Machine
Section titled “Comparison: Store Machine vs Factory Machine”| Feature | Store Machine | Factory Machine |
|---|---|---|
| State type | Single value | Discrete states |
| API | dispatch(action, ...params) | send(event, ...params) |
| State access | getState() returns value | getState() returns state object |
| Pattern matching | N/A | state.match({...}) |
| Use case | App state, forms, counters | Workflows, async flows, UI modes |
Next Steps
Section titled “Next Steps”- Lifecycle & Hooks - Add side effects to your store
- Effects - Declarative effect management
- Factory Machines - For discrete state management