Skip to content

Selectors & Computed State

StateMesh provides two ways to derive state: selectors (functions that extract and transform state) and computed values (cached derived state with automatic dependency tracking).

Selectors

A selector is a function that extracts a piece of state:

ts
const cartCount = (state) => state.cart.items.reduce((sum, item) => sum + item.quantity, 0);
const cartTotal = (state) => state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);

useMeshSelector

Use useMeshSelector in React components to subscribe to derived state:

tsx
import { useMeshSelector } from "statemesh-core";

function CartBadge() {
  const count = useMeshSelector((state) =>
    state.cart.items.reduce((sum, item) => sum + item.quantity, 0)
  );
  return <span>{count} items</span>;
}

The component only re-renders when the selector's return value changes.

createSelector

For expensive selectors, use createSelector to memoize the result:

tsx
import { createSelector } from "statemesh-core";

const selectCartTotal = createSelector(
  (state) => state.cart.items,
  (items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

function CartTotal() {
  const total = useMeshSelector(selectCartTotal);
  return <strong>Total: ${total.toFixed(2)}</strong>;
}

createSelector takes one or more input selectors followed by a combiner function. It caches the last result and only recomputes when any input selector returns a new reference.

When to use createSelector

Use createSelector when:

  • The selector does expensive computation (sorting, filtering large arrays)
  • The selector is used in multiple components
  • You want to avoid unnecessary recomputation on unrelated state changes

Computed Values

Computed values are named, cached derived state registered on the mesh. They automatically track their dependencies and recompute only when those dependencies change.

Define a Computed Value

ts
mesh.computed("cart.total", {
  deps: ["cart.items"],
  compute: (state) => {
    return state.cart.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }
});

Use in React

tsx
import { useMeshComputed } from "statemesh-core";

function CartTotal() {
  const total = useMeshComputed<number>("cart.total");
  return <strong>Total: ${total.toFixed(2)}</strong>;
}

How Dependencies Work

The deps array specifies which state paths the computed value depends on. When any of these paths change, the computed value is marked dirty and recomputes on the next read.

ts
mesh.computed("user.displayName", {
  deps: ["user.firstName", "user.lastName"],
  compute: (state) => `${state.user.firstName} ${state.user.lastName}`
});

TIP

Computed values are lazy — they don't compute until someone reads them. If no component or code reads the value, no computation happens.

Dependency Intersection

If your computed value depends on paths that are subsets of each other, StateMesh uses dependency intersection to avoid redundant recomputation:

ts
mesh.computed("cart.summary", {
  deps: ["cart.items", "cart.items.length"],
  compute: (state) => ({
    count: state.cart.items.length,
    total: state.cart.items.reduce((sum, item) => sum + item.price, 0)
  })
});

Registration and Replacement

Like actions, computed values are guarded against duplicate registration:

ts
mesh.computed("cart.total", { deps: [...], compute: ... });

// Throws DuplicateRegistrationError
mesh.computed("cart.total", { deps: [...], compute: ... });

// Replace explicitly
mesh.computed("cart.total", { deps: [...], compute: ... }, { replace: true });

Example: Product Catalog

ts
const mesh = createMesh({
  state: {
    products: [] as Product[],
    filters: { search: "", category: "all", sort: "name" }
  }
});

// Computed: filtered and sorted products
mesh.computed("products.filtered", {
  deps: ["products", "filters"],
  compute: (state) => {
    let result = state.products;

    if (state.filters.search) {
      const query = state.filters.search.toLowerCase();
      result = result.filter((p) => p.name.toLowerCase().includes(query));
    }

    if (state.filters.category !== "all") {
      result = result.filter((p) => p.category === state.filters.category);
    }

    if (state.filters.sort === "price") {
      result = [...result].sort((a, b) => a.price - b.price);
    } else {
      result = [...result].sort((a, b) => a.name.localeCompare(b.name));
    }

    return result;
  }
});

// Computed: total count
mesh.computed("products.count", {
  deps: ["products"],
  compute: (state) => state.products.length
});

Important Notes

TIP

Computed values are cached until their dependencies change. Multiple reads return the same cached result without recomputing.

WARNING

If the compute function throws, the computed value notifies listeners with the last known good value and marks itself dirty for the next read.

WARNING

Avoid side effects in compute functions. They should be pure transformations of state.

Next Steps

Released under the MIT License.