Core Concepts
StateMesh is built on a few core ideas. Understanding them will help you use the library effectively.
The External Store
StateMesh uses an external store — state lives outside React, in a plain JavaScript object. React components read from this store via hooks, but the store itself has no dependency on React.
const mesh = createMesh({
state: { count: 0 }
});
// Works without React
mesh.getState(); // { count: 0 }
mesh.setState({ count: 1 });
mesh.setPath("count", 2);This means you can use the store anywhere: in event handlers, API callbacks, WebSocket listeners, service workers, or even in non-React parts of your app.
Path-Based Subscriptions
When you subscribe to state, you provide a selector — a function that extracts a piece of state:
mesh.subscribe(
(state) => state.cart.items.length,
(count) => console.log("Cart has", count, "items")
);The callback only fires when the selected value actually changes (compared by reference by default, or by a custom equality function). This means:
- Updating
cart.statusdoes not trigger subscribers readingcart.items.length - Updating
themedoes not trigger subscribers readinguser.name - Components only re-render when their selected data changes
This is how StateMesh avoids unnecessary re-renders without requiring memoization, selectors, or careful state structuring.
Actions Are Named Mutations
Actions are the primary way to change state. Each action has a name and a handler that receives a draft copy of the state:
mesh.action("cart.addItem", (state, product: Product) => {
state.cart.items.push({ ...product, quantity: 1 });
});The handler receives a draft — a mutable proxy. You write imperative mutation code, but the original state object is never touched. StateMesh applies the changes atomically after the handler returns.
Why named?
Names serve three purposes:
- DevTools — every action appears in the timeline with its name
- Guards — you can block actions by name pattern
- Testing — you can mock actions by name
Transactions Are the Unit of Work
Every state change in StateMesh goes through a transaction. A transaction has a clear lifecycle:
before() → Validate preconditions
optimistic() → Update UI immediately
effect() → Call API, write to disk, etc.
commit() → Apply the real result
rollback() → Undo the optimistic update on failureThis lifecycle is automatic. You don't need to manage loading states, error states, or rollback logic manually — the transaction handles it.
mesh.transaction("cart.checkout", {
optimistic(state) {
state.cart.status = "processing";
},
async effect(state, payload, ctx) {
return fetch("/api/checkout", { signal: ctx.signal });
},
commit(state, result) {
state.cart.status = "completed";
state.order = result;
},
rollback: true
});The Event System
Every state change emits an event. Events are the backbone of StateMesh's plugin system, middleware, guards, and DevTools.
// Listen to all state changes
mesh.subscribe((state) => state, (state) => { /* ... */ });
// Listen to specific events
mesh.on({ type: "action.completed" }, (event) => {
console.log("Action completed:", event.name);
});
// Listen with wildcards
mesh.on({ type: "resource.*" }, (event) => {
console.log("Resource event:", event.type);
});Events have a discriminated union type — TypeScript narrows the event shape based on the type field.
Plugins Extend the Mesh
Plugins add capabilities to the mesh. They have a lifecycle: setup runs when the plugin is registered, cleanup runs when the mesh is destroyed.
mesh.use({
name: "my-plugin",
setup(mesh) {
// Subscribe to events, register middleware, etc.
const unsub = mesh.on({ type: "state.changed" }, (event) => {
analytics.track("state_change", event);
});
return () => unsub(); // cleanup
}
});Persistence, tab sync, and DevTools are all implemented as plugins.
Composition Over Configuration
StateMesh doesn't use a single configuration object. Instead, you compose features imperatively:
const mesh = createMesh({ state: { /* ... */ } });
// Add features one by one
mesh.action("theme.toggle", toggleHandler);
mesh.transaction("cart.checkout", checkoutDef);
mesh.computed("cart.total", { deps: ["cart.items"], compute: totalFn });
mesh.form("profile.form", formDef);
mesh.persist({ storage: "localStorage", keys: ["theme"] });
mesh.use(tabSyncPlugin({ keys: ["theme"] }));
mesh.use(loggerPlugin({ enabled: true }));
mesh.middleware(analyticsMiddleware);
mesh.guard({ kind: "action", name: /^admin\./ }, adminGuard);Each feature is independent. You only add what you need.
Next Steps
- TypeScript — Type inference patterns and generic usage
- State — Deep dive into state reads, writes, and subscriptions
- Actions — Named mutations with error handling
