Skip to content

React Adapter

Subscribe components to stores with useStoreState and related hooks. No provider needed.

Import from @nice-code/state/react. There is no provider: you pass the store directly. react >= 19 is an optional peer dependency.

import { useStoreState } from "@nice-code/state/react";
function Counter() {
// Select a slice — re-renders only when `count` changes.
const count = useStoreState(counterStore, (s) => s.count);
return (
<button onClick={() => counterStore.update((s) => { s.count += 1; })}>
{count}
</button>
);
}
// No selector → subscribe to the whole state.
const state = useStoreState(counterStore);

A selector can build a brand-new object or array on every call (a .filter, a .map, an object literal) without causing needless re-renders. The result is checked by reference (===) first. That is the fast path, and exactly right for slices of your state, because Immer reuses the objects that didn’t change. When the reference does differ, an equality function that defaults to deepEqual decides, so a structurally equal value doesn’t re-render:

import { useStoreState } from "@nice-code/state/react";
// Rebuilds a new array each call, but only re-renders when the filtered result
// actually changes — the deepEqual default absorbs the new-but-equal case.
const activeTodos = useStoreState(store, (s) => s.todos.filter((t) => !t.done));

store.watch() compares values the same way, so both ways of subscribing treat a recomputed-but-equal value alike. The deep comparison only runs on a genuine reference change and is bounded by the size of the selected slice. If a selector returns a very large slice on a hot path and you’d rather skip the traversal, pass a strict comparator to go back to reference-only checks:

const bigSlice = useStoreState(store, (s) => s.hugeThing, (a, b) => a === b);

useLocalStore makes a store that belongs to a single component instance and persists across renders. Pass a deps array to rebuild it, with fresh initial state, whenever those dependencies change.

import { useLocalStore, useStoreState } from "@nice-code/state/react";
function Editor({ docId }: { docId: string }) {
const store = useLocalStore(() => ({ draft: "" }), [docId]);
const draft = useStoreState(store, (s) => s.draft);
// ...
}

InjectStoreState subscribes inline without writing a hook:

import { InjectStoreState } from "@nice-code/state/react";
<InjectStoreState store={counterStore} on={(s) => s.count}>
{(count) => <span>{count}</span>}
</InjectStoreState>