Skip to content

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.

ts
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:

ts
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.status does not trigger subscribers reading cart.items.length
  • Updating theme does not trigger subscribers reading user.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:

ts
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:

  1. DevTools — every action appears in the timeline with its name
  2. Guards — you can block actions by name pattern
  3. 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 failure

This lifecycle is automatic. You don't need to manage loading states, error states, or rollback logic manually — the transaction handles it.

ts
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.

ts
// 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.

ts
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:

ts
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

Released under the MIT License.