# @nice-code/state — documentation
A small reactive state store (built on Immer) that only re-renders what changed.
This file concatenates only the @nice-code/state pages of https://nicecode.io — point a local AI
coding assistant at it when you're working with just this package. For the full set (every
nice-code package and how they fit together) use /llms.txt.
Generated from src/content/docs — do not edit by hand.
---
# Patches & Devtools
Source: /nice-state/patches
Description: Immer patch streams for undo/redo and sync, plus the state inspector panel.
## Patches
Every update can produce [Immer patches](https://immerjs.github.io/immer/patches/) — a small description of exactly what changed. Patches power undo/redo, syncing, and the devtools. They're only built when something is actually listening for them, so you pay nothing for them the rest of the time.
```ts
import { applyPatchesToStore } from "@nice-code/state";
// Listen to patches for every update
const stop = store.listenToPatches((patches, inversePatches) => {
sendToServer(patches);
});
// Or collect patches for a single update
store.update((s) => { s.count += 1; }, (patches, inverse) => {
undoStack.push(inverse);
});
// Apply patches (received from elsewhere, or to undo)
store.applyPatches(patches);
```
Inverse patches are how undo works: keep a stack of them, and `applyPatches` the top one to undo the last change.
## Devtools
An inspector for your stores that needs no setup — a timeline of changes, before/after diffs, a live view of your state you can edit directly, and one-click undo using patches. Name the stores you want to observe; nothing renders inside your app.
```ts
// devtools.ts
import { createNiceDevtools } from "@nice-code/devtools";
import { counterStore } from "./counterStore";
export const devtools = createNiceDevtools({
topologyId: "my-app",
stores: { counterStore }, // the key is the label the panel shows
});
```
Underneath that is a `StateDevtoolsCore` (`registerStore("counterStore", counterStore)`) streamed through `stateBridgeScope`, both from `@nice-code/state/devtools/browser`. A store created later joins with `devtools.registerStore(…)`.
The panel — rendered in [the devtools window](/devtools/window/) — lets you filter by store, inspect and edit live state, view per-change diffs and patches, pause the timeline, and revert a change via its inverse patches.
> Registering a store attaches a patch listener (a negligible dev-time cost). Keep devtools setup out of production bundles.
## Exports
| Entry | Contents |
|---|---|
| `@nice-code/state` | `Store`, `update`, `applyPatchesToStore`, core types |
| `@nice-code/state/react` | `useStoreState`, `useLocalStore`, `InjectStoreState` |
| `@nice-code/state/devtools/browser` | `StateDevtoolsCore`, `NiceStateDevtools` |
---
# React Adapter
Source: /nice-state/react
Description: Subscribe components to stores with useStoreState and friends — no provider.
Import from `@nice-code/state/react`. No provider needed — pass the store directly. `react >= 19` is an optional peer dependency.
## useStoreState
```tsx
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 (
);
}
```
```tsx
// No selector → subscribe to the whole state.
const state = useStoreState(counterStore);
```
## Selecting computed values
A selector's result is checked by reference (`===`) first — 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, the result falls through to an equality function that **defaults to `deepEqual`**, so a selector that builds a **brand-new** object or array every call (a `.filter`, a `.map`, an object literal) doesn't cause a needless re-render when the value is structurally the same:
```tsx
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));
```
This matches `store.watch()`'s equality behavior, so both ways of subscribing treat a recomputed-but-equal value the same. 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 opt back into reference-only checks:
```tsx
const bigSlice = useStoreState(store, (s) => s.hugeThing, (a, b) => a === b);
```
## Stores that live inside one component
`useLocalStore` makes a store that belongs to a single component instance and sticks around across renders. Pass a `deps` array to rebuild it (with fresh initial state) whenever those dependencies change.
```tsx
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);
// ...
}
```
## Render-prop binding
`InjectStoreState` subscribes inline without writing a hook:
```tsx
import { InjectStoreState } from "@nice-code/state/react";
s.count}>
{(count) => {count}}
```
---
# Reactions & Watch
Source: /nice-state/reactions
Description: Derive state from state, and run side-effects outside React.
## Reactions — build state from other state
A reaction watches one slice of state and, when it changes, runs a function (inside Immer) to update other fields on the **same** store. Reactions run before any subscribers are notified, so these derived fields are always up to date within a single render.
```ts
interface IState {
count: number;
doubled: number; // derived — never written by hand
history: number[]; // derived
}
const store = new Store({ count: 0, doubled: 0, history: [] });
store.createReaction(
(s) => s.count, // watch
(count, draft) => { // derive
draft.doubled = count * 2;
draft.history = [...draft.history, count].slice(-12);
},
{ runNow: true }, // run once immediately to seed derived state
);
```
`createReaction` returns a disposer to remove the reaction.
## Watch — side-effects outside React
`watch` follows a slice of state and runs your callback only when that slice actually changes. Great for logging, saving to storage, or keeping non-React code in sync.
```ts
const unsubscribe = store.watch(
(s) => s.count,
(count, allState, previousCount) => {
console.log(`count: ${previousCount} → ${count}`);
},
);
```
The low-level `store.subscribe(() => { ... })` runs on every update (no slice, no arguments) — it's the basic building block the React adapter is built on.
## Reaction vs watch vs subscribe
| API | Runs when | Use it for |
|---|---|---|
| `createReaction` | a watched slice changes | building more state _onto the same store_ |
| `watch` | a watched slice actually changes | side-effects (logging, saving, syncing) |
| `subscribe` | every update | the low-level building block; for writing adapters |
---
# Stores
Source: /nice-state/stores
Description: Create an Immer-backed store and mutate it through drafts.
`@nice-code/state` is a small state store that works with any framework (built on Immer). It lets components subscribe to just the slice of state they care about, derive state from other state, stream changes as patches, and — if you use React — comes with a React adapter.
```bash
bun add @nice-code/state immer
```
## Why use it
- **One store, updated the easy way** — write normal mutations (`s.count += 1`) and Immer turns them into a new immutable state for you, reusing the parts that didn't change.
- **Updates that change nothing cost nothing** — subscribers are only notified when the state actually changes.
- **Subscribe to a slice, not the whole thing** — a component or side-effect only re-runs when the specific slice it watches changes.
- **Plays nicely with React** — built on `useSyncExternalStore`, with no provider or context to set up.
## Create a store
Pass an initial value, or a function that builds one (handy for SSR and resetting).
```ts
import { Store } from "@nice-code/state";
interface ICounterState {
count: number;
step: number;
}
export const counterStore = new Store({ count: 0, step: 1 });
```
## Update state
Updates run through Immer — change the `draft` however you like, and the store saves a new immutable state from it.
```ts
// Single update
counterStore.update((s) => {
s.count += s.step;
});
// Batched — applied in order, committed as one change
counterStore.update([
(s) => { s.count += 1; },
(s) => { s.count += 1; },
]);
// Subscribers and React components (`useStoreState`) are notified only ONCE, at the end of
// the batch — never on the intermediate state — which avoids extra/tearing renders.
// Replace the whole state
counterStore.replace({ count: 0, step: 1 });
// Replace by mapping from current state
counterStore.replaceFromCurrent((s) => ({ ...s, count: 0 }));
```
An updater's second argument is a read-only snapshot of the state from *before* this update:
```ts
counterStore.update((draft, original) => {
draft.count = original.count * 2;
});
```
## Read state
```ts
const { count } = counterStore.state;
```
The `state` getter returns the current state, frozen for read safety — writes must go through `update`/`replace`.
> Only read `store.state` in plain logic. In components use [`useStoreState`](/nice-state/react/); for side-effects use [`watch`](/nice-state/reactions/).
## SSR
On the server, `useStoreState` just reads the store's current value (it doesn't subscribe). Build your stores with a function (`new Store(() => initialState())`) so each request can start with its own fresh state.