# zustand · GPT-6.1 Sol # React quick start Build a typed React counter that selects state and calls a store action without a provider. Your store is a hook: create it with state and actions, then select what each component needs. ## Prerequisites Use an existing browser-based React TypeScript project with: - React 18 or later, with `react-dom` for rendering. - React types (`@types/react`) 18 or later and the corresponding React DOM types. - TypeScript 4.5 or later. - Node.js 12.20.0 or later, as required by Zustand's package; your project's tooling can require a newer version. These examples use the React JSX transform and a bundler that loads a TypeScript entry point from HTML. ## Install Zustand Install the [`zustand`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand) package in your project: ```bash npm install zustand ``` ## 1. Create a typed store Use [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) with the curried TypeScript form, `create()(…)`. Define your application's state and action together. The returned `useCounterStore` is the hook your components call. ```ts title="src/store.ts" import { create } from 'zustand' type CounterState = { count: number increment: () => void } export const useCounterStore = create()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) ``` The store starts with `count` set to `0`. The functional update reads the current count and adds one. `set` merges the returned object into the state, so updating `count` keeps the `increment` action. ## 2. Select state and actions in components Call the store hook with a selector. `Counter` receives the selected number; `Controls` receives the action function and passes it to the button's `onClick` handler. ```tsx title="src/App.tsx" import { useCounterStore } from './store' function Counter() { const count = useCounterStore((state) => state.count) return

Count: {count}

} function Controls() { const increment = useCounterStore((state) => state.increment) return } export default function App() { return (
) } ``` Both components use the same store without a Zustand provider. When `count` changes, `Counter` re-renders with the new value. Each selector subscribes its component to the value it returns rather than the entire state. ## 3. Mount the counter Give React a root element and load your entry point. In your project's HTML, use: ```html title="index.html" Zustand counter
``` Render `App` into that element: ```tsx title="src/main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' const container = document.getElementById('root')! createRoot(container).render( , ) ``` Start your project's development server and open its browser URL. You see **Count: 0** and a **+1** button. Click **+1**: the heading changes to **Count: 1**. Each subsequent click increases the displayed count by one. You now have a typed store with colocated state and an action, two selector-based components, and a mounted counter without a provider. See the [live Zustand demo](https://zustand-demo.pmnd.rs/) for another counter example. ## Where to go next - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) explains immutable updates and merging. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) explains selecting multiple values and keeping selector outputs stable. - [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works) connects the store, actions, and component subscriptions. # How Zustand works Zustand is a state-management library that keeps state and actions in a store and connects React components to it through selector subscriptions. ## Your store is a hook Pass a state creator to [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create). It returns a hook bound to the store, with store methods attached. Your state can contain primitives, objects, and functions; actions are functions that update that state. Use the hook in components without a provider. This browser example renders a count starting at `0`. Click **+1** to increment it. The counter selects the count and the action separately, following the React starter's pattern. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CounterState = { count: number inc: () => void } const useCounterStore = create()((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) return
{count}
} const container = document.createElement('div') document.body.append(container) createRoot(container).render() ``` The hook connects your component to an underlying store. An action updates the store; the subscription lets React read the selected value again. ```mermaid flowchart LR C["Component"] -->|calls| A["Store action"] A -->|set| S["Store state"] S -->|notifies| H["Hook subscription"] H -->|selector output| C ``` See the [live counter demo](https://zustand-demo.pmnd.rs/) for this state-and-action pattern in a rendered application. For project setup, start with the [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start). ## State is immutable; updates merge at one level Use `set` inside the state creator's actions, or `setState()` on the store, to update state and notify listeners. When the next value depends on the previous state, pass a function to `set`. For object state, the default update shallowly merges the returned fields into the existing state, so the counter action above keeps `inc` without spreading the entire state. Copy nested objects explicitly. Here **+1** updates `nested.count` while keeping `nested.label`; the screen starts at `Visits: 0`. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type VisitState = { nested: { count: number; label: string } inc: () => void } const useVisitStore = create()((set) => ({ nested: { count: 0, label: 'Visits' }, inc: () => set((state) => ({ nested: { ...state.nested, count: state.nested.count + 1 }, })), })) function Visits() { const nested = useVisitStore((state) => state.nested) const inc = useVisitStore((state) => state.inc) return (
{nested.label}: {nested.count}
) } const container = document.createElement('div') document.body.append(container) createRoot(container).render() ``` For deeper structures, see [Update nested state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-nested-state). [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) covers object, `Map`, and `Set` updates; [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) covers replacement updates. ## Selectors define the subscription Select what your component needs instead of reading the whole store. React compares the selected snapshot with `Object.is`: a changed selector output triggers a store-driven re-render. For computed outputs, [`useShallow`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-react-shallow#useshallow) returns a selector that preserves the previous output reference when the next output is shallowly equal. This makes an array or object selection stable when its contents have not changed. Install React explicitly: it is an optional peer dependency, so installing Zustand alone does not add it. ```bash npm install zustand react react-dom ``` This example follows the bear-meals example: it renders the bear names and papa bear's meal. Click **Order pizza** to change the meal. The names remain the same, and `BearNames` does not re-render because of that store update. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' const useMeals = create(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) function BearNames() { const names = useMeals(useShallow((state) => Object.keys(state))) return

{names.join(', ')}

} function Meal() { const meal = useMeals((state) => state.papaBear) return (

Papa bear's meal: {meal}

) } const container = document.createElement('div') document.body.append(container) createRoot(container).render(<>) ``` See [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) for selector stability in v5, and [Prevent unnecessary rerenders](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/prevent-unnecessary-rerenders) for equality choices. ## The store exists independently of React [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore) returns a standalone store, not a hook. Import it from `zustand/vanilla` when you need the core without a React dependency. [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore) connects a React component to that store and returns its selected state. The store exposes the [`StoreApi`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#storeapi) methods: | Method | What you get | | --- | --- | | `getState()` | The current state, without subscribing. | | `setState()` | A state update, with the same merging behavior as `set`. It returns `void`. | | `subscribe(listener)` | A subscription receiving current and previous state on updates. It returns an unsubscribe function. | | `getInitialState()` | The state returned by the initial state creator. | In this browser example, the button updates the vanilla store through `setState()`. The mounted component observes that same store through `useStore`, so the displayed count increases. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' const counterStore = createStore(() => ({ count: 0 })) function Counter() { const count = useStore(counterStore, (state) => state.count) return (
{count}
) } const container = document.createElement('div') document.body.append(container) createRoot(container).render() ``` For dependency injection or initialization from component props, put a vanilla store in React context and select from it with `useStore`, rather than passing the bound hook as a context value. See [Initialize scoped stores](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/initialize-scoped-stores) for that pattern, [Subscribe outside React](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/subscribe-outside-react) for listener lifetimes, and [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup) for server-side scope. ## Compose actions and slices into one store The recommended Flux-inspired pattern keeps application global state in a single store with colocated actions. You call those functions directly; dispatched actions and reducers are not required. Slices keep that store modular without creating separate subscriptions to separate stores. Type a slice creator with [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator): its first type argument describes the combined state, and its fourth describes the slice it returns. Pass the same `set`, `get`, and store arguments to every creator. Each slice can then update the combined state or call another slice's actions through `get()`. This example renders `0 bears, 0 fishes`. **Add both** calls the bear and fish actions through the shared slice, increasing both counts. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { create, type StateCreator } from 'zustand' type BearSlice = { bears: number; addBear: () => void } type FishSlice = { fishes: number; addFish: () => void } type SharedSlice = { addBoth: () => void } type ZooState = BearSlice & FishSlice & SharedSlice const createBearSlice: StateCreator = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), }) const createFishSlice: StateCreator = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const createSharedSlice: StateCreator = (_set, get) => ({ addBoth: () => { get().addBear() get().addFish() }, }) const useZooStore = create()((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), ...createSharedSlice(...args), })) function Zoo() { const bears = useZooStore((state) => state.bears) const fishes = useZooStore((state) => state.fishes) const addBoth = useZooStore((state) => state.addBoth) return (

{bears} bears, {fishes} fishes

) } const container = document.createElement('div') document.body.append(container) createRoot(container).render() ``` Continue with [Split a store into slices](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/split-a-store-into-slices) for composition, or [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) for middleware placement and slice typing. # State and actions A state creator defines a store's initial data and the actions that update it, keeping values and update functions together. Pass that function to [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) to get a store bound to a React hook. Your store can contain primitives, objects, and functions. Select the data or action each component needs; no provider is required. ## How updates flow Keep application global state in a single store and colocate its actions. This Flux-inspired pattern does not require dispatched actions or reducers: an action is a function that calls `set`. For a larger store, [compose slices](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/split-a-store-into-slices) without changing that model. ```mermaid flowchart LR A["State creator: data and actions"] --> B["create()"] B --> C["Bound store hook"] C --> D["Component selects data and actions"] D --> E["User calls an action"] E --> F["set() computes the next state"] F --> G["Merge or replace, then notify listeners"] G --> D ``` The creator receives `set`, `get`, and the store API. `set` updates state, while `get` reads the current state when an action runs. See [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state) for initialization constraints. Use a functional update when a value depends on previous state. Zustand calls the updater with the current state, computes the next state, and notifies listeners if that result differs from the current state by `Object.is`. ## Data and actions in code This browser example displays a heading and a nested counter. Clicking **+1** increases the count while preserving the nested label. Editing the heading changes only that top-level field. Clicking **Use default heading** updates the same mounted store through `setState` rather than a colocated action. Type the creator with [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator). The following two files belong in a React TypeScript project with `zustand` installed and a `
` in its HTML. ```ts title="store.ts" import { create, type StateCreator } from 'zustand' type CounterState = { heading: string nested: { count: number; label: string } } type CounterActions = { inc: () => void setHeading: (heading: string) => void } type CounterStore = CounterState & CounterActions const counterCreator: StateCreator = (set) => ({ heading: 'Daily count', nested: { count: 0, label: 'Clicks' }, inc: () => set((state) => ({ nested: { ...state.nested, count: state.nested.count + 1 }, })), setHeading: (heading) => set({ heading }), }) export const useCounterStore = create()(counterCreator) ``` ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { useCounterStore } from './store' function Counter() { const heading = useCounterStore((state) => state.heading) const nested = useCounterStore((state) => state.nested) const inc = useCounterStore((state) => state.inc) const setHeading = useCounterStore((state) => state.setHeading) return (

{heading}

{nested.label}: {nested.count}

) } createRoot(document.getElementById('root')!).render() ``` The hook returns each selector's result. The same bound hook also exposes the [`StoreApi`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#storeapi) methods: `setState`, `getState`, `getInitialState`, and `subscribe`. Its combined hook-and-API type is [`UseBoundStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#useboundstore). Inside the creator, `set` is the store's `setState` function. Both update paths therefore use the same merging and notification behavior, and both return `void`. You can also put actions at module level and call `useCounterStore.setState` there; selecting an action through the hook is not required for that pattern. ## Merge versus replace For object state, `set({ heading })` shallowly merges the update with the current store. You do not need to spread the entire state: fields omitted from the update remain unchanged, including the action functions. | Update | Result | | --- | --- | | Object update with `replace` omitted | Merge at the top level. | | Update with `replace: false` as the second argument | Explicitly request merging. | | Update with `true` as the second argument | Replace instead of merging. | | Non-object or `null` update with `replace` omitted | Replace the state. | The replacement flag is a positional boolean, not an options object. See [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) for complete-state replacement and its typing requirements. ## Why nested updates need new references `set` merges only one level. It does not merge fields inside `nested`. In `inc`, the spread copies the nested object and preserves `label`, then overwrites `count`. For deeper objects, copy every level along the changed path. See [Update nested state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-nested-state) for deeper updates and immutable-update helpers. Treat `Map` and `Set` the same way: create new instances for updates, using `new Map(state.map).set(key, value)` or `new Set(state.set).add(item)`. Mutating an existing collection preserves its reference, so a component selecting that collection does not get the expected re-render. A new top-level update alone does not fix an unchanged selected collection reference. For subscription details, continue with [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). To see nested updates running, open the [live nested-state demo](https://stackblitz.com/edit/vitejs-vite-j6bjdygu). # Selectors and subscriptions A selector picks the state a component needs; a subscription connects that component, or another listener, to future store updates. Your store is a hook: create it with state and actions, then select what each component needs. A snapshot read answers “what is the state now?” without subscribing. A hook subscription keeps the selected value current, and selector-output equality determines whether a store update causes a React re-render. ## How updates reach a component [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) returns a hook with store methods attached. Calling its `getState()` method reads the current state without registering a listener. Calling the hook in a component subscribes through React and returns the selector's output. ```mermaid flowchart TD A["Action calls set()"] --> B["Store updates state and notifies listeners"] B --> C["React reads the selector output"] C --> D["Compare output with Object.is"] D --> E["Different: re-render with the new output"] D --> F["Equal: no store-driven re-render"] B --> G["Explicit subscribe() listener runs"] H["getState()"] --> I["Current snapshot, no subscription"] ``` React compares computed selector outputs with `Object.is`. Selecting a number or an existing action function works without constructing a wrapper. Calling the hook without a selector returns the entire state, so every change to that state can cause a re-render. ## Compare a snapshot with a subscription Use this as `src/App.tsx` in your React application. It follows the starter's separate selectors for state and actions. The screen shows a live count, a count captured at mount, and an increment button. ```tsx title="src/App.tsx" import { useEffect, useState } from 'react' import { create } from 'zustand' type CounterStore = { count: number inc: () => void } const useCounterStore = create()((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) export default function App() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) const [countAtMount] = useState(() => useCounterStore.getState().count) useEffect(() => { const unsubscribe = useCounterStore.subscribe((state, previousState) => { console.log('Count changed:', previousState.count, '→', state.count) }) return unsubscribe }, []) return (

Live count: {count}

Count at mount: {countAtMount}

) } ``` Click **Increment**. The live count changes; the count captured at mount stays unchanged. Each later `getState()` call reads the latest state, but the number already captured does not update itself. The explicit `subscribe()` listener runs synchronously when the store changes and receives both the new and previous full state. It returns an unsubscribe function, which the effect uses as cleanup on unmount. Registering this listener does not itself request a React render; the count selector provides that connection in this sample. ## Keep computed outputs stable [`useShallow`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-react-shallow#useshallow) returns a selector that reuses its previous output reference when the new output is shallowly equal. This lets you construct an object or array from several state picks without treating an equivalent wrapper as a new value. Install React explicitly if your project does not already include it; it is an optional peer dependency of Zustand: ```bash npm install zustand react react-dom ``` The bear-meals example illustrates the distinction: changing a meal changes the store, but not the list of bear names. Replace `src/App.tsx` with this complete example. It renders the names, papa bear's meal, and a button to change that meal. Here, [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore) creates a vanilla store and [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore) connects its selected values to React. ```tsx title="src/App.tsx" import { createStore, useStore } from 'zustand' import { useShallow } from 'zustand/react/shallow' type BearMeals = { papaBear: string mamaBear: string littleBear: string } const mealsStore = createStore()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) function BearNames() { const names = useStore(mealsStore, useShallow((state) => Object.keys(state))) return

Bear names: {names.join(', ')}

} function MealControls() { const meal = useStore(mealsStore, (state) => state.papaBear) return (

Papa bear's meal: {meal}

) } export default function App() { return (
) } ``` Click **Order pizza**. The meal text changes to `a large pizza`; the names remain `papaBear, mamaBear, littleBear`. Although `Object.keys()` constructs a new array, its entries are unchanged. `useShallow` returns the previous array reference, so this meal update does not cause a store-driven re-render of `BearNames`. Shallow comparison checks top-level values, not nested contents recursively. It does not stop re-renders caused by a component's parent or its own React state. ### Avoid fresh references in v5 selectors In v5, a selector that returns a fresh object, array, or fallback function on every read can cause an infinite update loop. Keep the **output** reference stable: use separate selectors for individual values, wrap shallowly equal computed outputs in `useShallow`, or declare a fallback function outside the component rather than creating it inside the selector. This applies both to bound hooks from `create` and to `useStore`. For custom equality functions and further rendering techniques, see [Prevent unnecessary rerenders](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/prevent-unnecessary-rerenders). ## The API involved - `getState()` returns the current state without subscribing. Use it in event handlers or other imperative code when you need a fresh snapshot. - The hook returned by `create` selects and subscribes in React without a provider. With a vanilla store from `createStore`, use `useStore(store, selector)` for that connection instead. - `subscribe(listener)` listens to full-state changes and returns an unsubscribe function. It does not call the listener at registration. - [`subscribeWithSelector`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#subscribewithselector) adds `subscribe(selector, listener, options)` for imperative listeners interested in a slice. The listener receives the new and previous selected values. Its `equalityFn` defaults to `Object.is`; `fireImmediately` optionally calls the listener at registration with the current selected value as both arguments. See [Subscribe outside React](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/subscribe-outside-react) for setup and cleanup. - `useShallow(selector)` preserves a shallowly equal output reference so React's output comparison can recognize it as unchanged. Try the [live Zustand demo](https://zustand-demo.pmnd.rs/) to see a store-backed counter, or continue with [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) for updating the state your selectors read. # Store scope and lifecycle Store scope is the boundary you choose for sharing a store instance; its lifetime depends on where your application keeps that instance. The store exists independently of React, and hooks connect React components to it. Creating a store, subscribing a component, and hydrating persisted state are separate operations: mounting a consumer does not create a new instance of an existing store. ## Ownership determines sharing [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) creates a store and returns a hook bound to it. Every component calling that same hook reads the same store. A module-level hook therefore gives the components importing it shared state without a provider. [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore) returns a vanilla store instead of a bound hook. Call it inside a factory when you need independent instances. For dependency injection or initialization from component props, keep a vanilla store in React context. [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore) connects a consumer to the instance supplied by that context and returns its selected state. ```mermaid flowchart TD M["Module-level create()"] --> G["Shared store and bound hook"] G --> A["Consumer A"] G --> B["Consumer B"] P["Provider with lazy useState initializer"] --> V["Vanilla store instance"] V --> C["Context supplies the instance"] C --> D["useStore(instance, selector)"] D --> E["Scoped consumer"] S["Persisted storage"] -->|"Hydration merges saved state"| V ``` Choose the owner according to the sharing you want: - **Module:** components using the same exported hook or vanilla store share one instance. Unmounting a consumer does not remove the module's reference to that store. - **Provider:** descendants share the provider's instance. Separate providers can own separate instances, even when they use the same factory. - **Keyed collection:** a map can retain one instance per tab or other key. Switching consumers does not remove the entries; the collection owns those references. The authors' dynamic-store example uses a map to reuse each tab's store. A component subscription is not an ownership mechanism: removing the subscription does not reset the state. ## A module-level store shares updates In a browser React project with a `root` element, use this as `src/main.tsx`. Both buttons start at zero. Clicking either button increments the count displayed by both, because both consumers use the same bound hook. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CounterState = { count: number increment: () => void } const useCounterStore = create()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) function Counter({ label }: { label: string }) { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) return ( ) } function App() { return (
) } createRoot(document.getElementById('root')!).render() ``` This follows the starter's module-level store and separate selectors for state and actions. The [live Zustand demo](https://zustand-demo.pmnd.rs/) also uses a module-level bound hook for its counter. ## A provider initializes an independent instance Use this alternative `src/main.tsx` to give each provider its own counter. The first provider starts at two and has two consumers; the second starts at ten and has one. Clicking either button in the first group updates both of that group's buttons, but leaves the second group unchanged. The lazy `useState` initializer keeps the instance stable across provider re-renders. `initialCount` supplies creation-time data, not an ongoing binding: changing that prop on the same mounted provider does not recreate or update its store. A new provider mount creates a new instance from its initial props. ```tsx title="src/main.tsx" import { createContext, useContext, useState, type ReactNode } from 'react' import { createRoot } from 'react-dom/client' import { createStore, useStore } from 'zustand' type CounterState = { count: number increment: () => void } function createCounterStore(initialCount: number) { return createStore()((set) => ({ count: initialCount, increment: () => set((state) => ({ count: state.count + 1 })), })) } type CounterStore = ReturnType const CounterContext = createContext(null) function CounterProvider({ initialCount, children, }: { initialCount: number children: ReactNode }) { const [store] = useState(() => createCounterStore(initialCount)) return ( {children} ) } function useCounter(selector: (state: CounterState) => T): T { const store = useContext(CounterContext) if (!store) throw new Error('Missing CounterProvider') return useStore(store, selector) } function Counter({ label }: { label: string }) { const count = useCounter((state) => state.count) const increment = useCounter((state) => state.increment) return ( ) } function App() { return (

First scope

Second scope

) } createRoot(document.getElementById('root')!).render() ``` Context carries the vanilla instance, not a bound hook. For a reusable provider pattern, continue with [Initialize scoped stores](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/initialize-scoped-stores). ## Initialization and persistence hydration are different The state creator runs when you create the store and returns its initial state and actions. A later component subscription reads that existing store; it does not run the state creator again. For construction-time constraints, see [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state). [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist) adds another stage: **hydration retrieves persisted state from storage and merges it with current state**. It does not change which components share the instance. - With synchronous storage such as `localStorage`, normal automatic hydration completes during store creation. - With asynchronous storage, hydration completes later, in a microtask. The initial React render can precede that completion. See [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) for handling that timing. - With `skipHydration: true`, automatic hydration at initialization is disabled. Call the instance's `persist.rehydrate()` when your application is ready; it returns `void` or a promise. See [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration). In-memory ownership and storage identity are separate. The persistence `name` identifies the storage entry and must be unique. Separate scoped instances using the same name read and write that same entry, even though their in-memory state is independent. ## Subscriptions connect React without owning the store The state and listeners behind [`StoreApi`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#storeapi) belong to the store instance, not its consumers; see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) for the individual methods. `useStore` uses React's `useSyncExternalStore` with the instance's subscription and snapshot methods. React manages that component's subscription lifecycle. Unmounting a consumer ends its subscription; it does not reset or delete the store. Select only the state each consumer needs, as in the samples above. See [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) for selector-output equality and rendering behavior. When you register a listener yourself with `subscribe()`, retain its unsubscribe function and call it when that listener's owner tears down. That removes the listener, not the state. See [Subscribe outside React](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/subscribe-outside-react) for an effect with cleanup. React's server snapshot uses `getInitialState()`. With persistence, that method returns the state creator's result, while hydration can change current state. Persistence hydration is not React's server/client hydration. For request ownership and matching server/client initialization, follow [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). # Update state with actions Use colocated actions when React controls need to update shared state. This example displays a count starting at `0`; the **+1** and **+5** buttons increase it from its previous value by the chosen amount. Create the store with [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create), then select the state and actions each component needs. The returned store is a hook, so you do not need a provider. Keep update functions alongside state rather than introducing dispatched actions or reducers for this task. ## 1. Define a typed action In your React TypeScript project, add `counter-store.ts`. If you have not installed Zustand, run: ```bash npm install zustand ``` Extend the [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start) pattern with an action argument: `inc` accepts a numeric amount and returns `void`. Use a functional update because the next count depends on the current store state. ```ts title="counter-store.ts" import { create } from 'zustand' type CounterStore = { count: number inc: (by: number) => void } export const useCounterStore = create()((set) => ({ count: 0, inc: (by) => set((state) => ({ count: state.count + by })), })) ``` `set` calls the updater with the current state, shallowly merges the returned object into the store, and notifies listeners. Returning only `{ count: ... }` preserves `inc`; you do not need to spread the whole store into this flat update. ## 2. Bind the action to a React control Use this browser entry point with an HTML element whose ID is `root`, as in the [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start). Keep the starter's separate selectors, and wrap each button handler to pass the amount to `inc` rather than the click event. ```tsx title="main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { useCounterStore } from './counter-store' function Counter() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) return (

Count: {count}

) } createRoot(document.getElementById('root')!).render( , ) ``` The mounted component displays **Count: 0** and **+1** and **+5** buttons. Clicking either button calls `inc` with its amount, changes the selected count, and updates the displayed number. Click **+1**, then **+5**: the displayed count changes to `1`, then `6`. The action returns no value; read the result through the count selector. ## Update choices Choose the update form according to where the next value comes from. | Option | Type | Default | What it does | | --- | --- | --- | --- | | Object passed to `set` | Partial store state | No default; supply an update | Sets known field values and shallowly merges them into the existing object state. | | Function passed to `set` | Current state → complete or partial store state | No default; supply an update | Computes the update from the current state, as `inc` does above. | | Second argument to `set` | `boolean` | Merges when omitted for object updates | `false` merges; `true` replaces the state. See [replacement typing and precautions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware). | ## Pitfalls - Keep the previous-value calculation inside the functional updater so it reads the store value at the time of the call. - For nested objects, follow [Update nested state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-nested-state) and the merging rules in [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions). - To select several values as one object or array, follow [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). This sample selects the count and action separately. ## Live demo Try the [live counter demo](https://zustand-demo.pmnd.rs/). It uses the same functional increment pattern as the sample's **+1** button. Browse the [demo source](https://github.com/pmndrs/zustand/blob/d7a5583cffd80af515f7dfb69583c95cbdc9e2ce/examples/demo) or the [typed React starter](https://github.com/pmndrs/zustand/blob/d7a5583cffd80af515f7dfb69583c95cbdc9e2ce/examples/starter) for the original examples. ## Related - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) explains immutable updates and merging. - [Split a store into slices](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/split-a-store-into-slices) keeps colocated state and actions modular as the store grows. - [Use reducers and dispatch](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/use-reducers-and-dispatch) covers the alternative reducer pattern. # Prevent unnecessary rerenders Use selectors to subscribe to the values your component renders. Select individual values when possible; wrap computed selections with [`useShallow`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-react-shallow#useshallow) when shallow comparison is sufficient. Use the traditional hooks when you need a custom equality check. Selector-output equality determines whether a store update causes a component to rerender. The default comparison is `Object.is`; `useShallow` returns a selector that reuses its previous output when the next output is shallowly equal. ## 1. Select individual values Create a bound store with [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create), then select state and actions separately. This browser example renders a counter and a text input. Changing the text updates `TextInput`, but does not cause a store-driven rerender of `Counter`: neither its selected count nor its action changes. Use each example below as your React project's `src/main.tsx`, with a `
` in its HTML entry point. Install Zustand and its React peer, plus React DOM to mount the examples: ```bash npm install zustand react react-dom ``` ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CounterStore = { count: number text: string inc: () => void setText: (text: string) => void } const useCounterStore = create()((set) => ({ count: 0, text: 'hello', inc: () => set((state) => ({ count: state.count + 1 })), setText: (text) => set({ text }), })) function Counter() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) return } function TextInput() { const text = useCounterStore((state) => state.text) const setText = useCounterStore((state) => state.setText) return ( ) } function App() { return
} createRoot(document.getElementById('root')!).render() ``` Clicking the counter increments the displayed count. Each hook returns its selected value, rather than the entire store. ## 2. Stabilize a computed selection When you need a computed array or object, pass the selector through `useShallow`. It compares the output, not the whole store, and returns the previous output reference when the comparison succeeds. For the store and selector pattern, see [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works#selectors-define-the-subscription). Run this comparison in your React project's development mode: React's `Profiler` logs commits for a whole-store subscription and a shallow-selected names subscription. Click **Order pizza**: papa bear's displayed meal changes, and both lists remain `papaBear, mamaBear, littleBear`. Look in the browser console for an `update` from `WholeStoreNames`, but not from `BearNames`: the shallow-selected array's entries have not changed. ```tsx title="src/main.tsx" import { Profiler } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type Meals = { papaBear: string mamaBear: string littleBear: string } const useMeals = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) function BearNames() { const names = useMeals(useShallow((state) => Object.keys(state))) return

{names.join(', ')}

} function WholeStoreNames() { const meals = useMeals() return

{Object.keys(meals).join(', ')}

} function PapaMeal() { const meal = useMeals((state) => state.papaBear) return (

Papa bear's meal: {meal}

) } function App() { return (
console.log(id, phase)}> console.log(id, phase)}>
) } createRoot(document.getElementById('root')!).render() ``` ![The bear names appear above papa bear's initial meal and the Order pizza button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/0a1fb276329fcab7846effd4e5530f96.png) The same wrapper works for multiple state picks in an object or array. For example, select both a value and its action inside the wrapped selector. Shallow comparison checks top-level entries with `Object.is`; it does not recursively compare nested objects. A newly created nested object still compares differently. ## 3. Use custom equality when shallow comparison is not enough In v5, `create` does not support a custom equality function. Use [`createWithEqualityFn`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-traditional#createwithequalityfn) from `zustand/traditional` for a bound hook with custom equality, or keep `create` and use `useShallow` for shallow comparison. Install the traditional entry point's peer dependency in your project: ```bash npm install zustand react react-dom use-sync-external-store ``` Pass the shipped [`shallow`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-shallow#shallow) comparator as the store's default equality function. You can override it for an individual subscription by passing a second argument to the bound hook. This example displays the number of completed groups of ten. Each click increments the store's count, but `GroupsOfTen` keeps its selected output while both counts belong to the same group. Its displayed value changes from zero to one on the tenth click. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type CounterStore = { count: number inc: () => void } const useCounterStore = createWithEqualityFn()( (set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), }), shallow, ) function sameGroup(previous: number, next: number) { return Math.floor(previous / 10) === Math.floor(next / 10) } function GroupsOfTen() { const count = useCounterStore((state) => state.count, sameGroup) return

Completed groups of ten: {Math.floor(count / 10)}

} function IncrementButton() { const inc = useCounterStore((state) => state.inc) return } function App() { return
} createRoot(document.getElementById('root')!).render() ``` Only ignore differences your component does not render. Here, the component renders the group number, not the raw count. For an existing vanilla store created with [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore), use [`useStoreWithEqualityFn`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-traditional#usestorewithequalityfn). Pass the store instance, a selector, and an optional equality function. It returns the selected data and applies that equality check to the subscription; you do not need to recreate the store as a bound hook. Use [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore) for subscriptions that do not need custom equality. This mounted example uses it to select the increment action and uses `useStoreWithEqualityFn` to select the count with the group comparison. Click **Add one** ten times to change the displayed group from zero to one. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { createStore, useStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' type CounterStore = { count: number inc: () => void } const counterStore = createStore()((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function sameGroup(previous: number, next: number) { return Math.floor(previous / 10) === Math.floor(next / 10) } function GroupsOfTen() { const count = useStoreWithEqualityFn( counterStore, (state) => state.count, sameGroup, ) return

Completed groups of ten: {Math.floor(count / 10)}

} function IncrementButton() { const inc = useStore(counterStore, (state) => state.inc) return } function App() { return
} createRoot(document.getElementById('root')!).render() ``` ## Comparison parameters These APIs take selector and comparison arguments rather than an options object. | Argument | Type | Default | What it does | | --- | --- | --- | --- | | `useShallow` selector | `(state: S) => U` | Required | Computes the output whose previous reference is reused when shallowly equal. | | `createWithEqualityFn` default equality | `(a: U, b: U) => boolean` | `undefined` | Supplies the comparison for bound-hook calls that do not pass their own equality function. | | Bound hook equality | `(a: U, b: U) => boolean` | Store's default equality | Overrides the comparison for that subscription. | | `useStoreWithEqualityFn` equality | `(a: U, b: U) => boolean` | `undefined` | Supplies the comparison for the selected output from a vanilla store. | ## Pitfalls and verification - Shallow equality is not deep equality. Select the primitive fields you render when nested object references change but those fields do not. - These comparisons prevent store-driven rerenders, not renders caused by a parent or local React state. Use React DevTools' Profiler to inspect which components commit when you click the examples' buttons. - For stable selector outputs in v5, see [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). ## Live demo and related guides See a bound store and counter running in the [Zustand live demo](https://zustand-demo.pmnd.rs/). - [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start) — set up your first store and React component. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) — define what each component subscribes to. - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) — preserve references correctly when updating state. # Persist store data Use [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist) when a store's data needs to survive a page reload. Wrap your state creator, choose a unique storage key, and use `partialize` to select the fields to save. The browser example below keeps a bear count across reloads while a temporary click count resets to zero. It uses React and the browser's `localStorage`. ## 1. Create a persisted store In your React TypeScript project, install Zustand: ```bash npm install zustand ``` Pass the wrapped state creator to [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create). `persist` returns a state creator; `create` returns the hook your components use. Use [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) to adapt browser storage to JSON reads and writes. ```ts title="store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' type BearStore = { bears: number temporaryClicks: number addABear: () => void } export const useBearStore = create()( persist( (set) => ({ bears: 0, temporaryClicks: 0, addABear: () => set((state) => ({ bears: state.bears + 1, temporaryClicks: state.temporaryClicks + 1, })), }), { name: 'bear-counter-storage', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ bears: state.bears }), }, ), ) ``` `name` is the storage key and the only required option. Give each persisted store a unique key so stores do not share the same storage entry. `partialize` picks the data written to storage, not the fields available to components. Here, the live store still contains `temporaryClicks` and `addABear`, but the persisted state contains only `bears`. Each action update writes the selected state under `bear-counter-storage`. ## 2. Render and test the counter Select the values and action separately, following the React starter's counter pattern. Use this file as your browser entry point: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { useBearStore } from './store' function App() { const bears = useBearStore((state) => state.bears) const temporaryClicks = useBearStore((state) => state.temporaryClicks) const addABear = useBearStore((state) => state.addABear) return (

Persisted bear counter

Bears: {bears}

Clicks since reload: {temporaryClicks}

) } createRoot(document.getElementById('root')!).render() ``` Use the HTML setup from [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start#3-mount-the-counter), with the script's `src` set to `/main.tsx` to load the entry point above. Click **Add a bear**: both counts increase. Reload the page: the bear count returns from storage, and **Clicks since reload** shows zero. Rehydration merges the saved fields into the initial store, retaining the action and the initial value for the omitted field. ## 3. Choose local or session storage For `localStorage`, keep the sample's `storage` option, or omit it to use the default adapter. For `sessionStorage`, replace that option with `storage: createJSONStorage(() => sessionStorage)`. The same counter and `partialize` function work with either storage engine. Both browser storage engines are synchronous, so Zustand hydrates during store creation. Use `sessionStorage` for data scoped to the browser page session; use `localStorage` when the data needs to remain after that session ends. ## Options that matter The types below describe the sample's full `BearStore` and selected persisted state. A storage adapter implements [`PersistStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persiststorage). | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Required; no default | Sets the unique storage key. | | `storage` | `PersistStorage<{ bears: number }>` | `createJSONStorage(() => localStorage)` | Selects the storage adapter used to read and write state. | | `partialize` | `(state: BearStore) => { bears: number }` | `(state) => state` | Filters the state before each write. | ## Pitfalls Asynchronous storage does not hydrate before the initial render. Defaults can therefore look like persisted values—for example, a logged-out state—while storage is still loading. Track hydration completion and wait before rendering UI that depends on persisted data. See [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration) for the hydration gate. For partially persisted nested objects and custom merging, see [Migrate persisted state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/migrate-persisted-state). For server-rendered React applications, see [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). ## Live demo and related Try the [live Zustand demo](https://zustand-demo.pmnd.rs/) for the hook-based counter interaction. The example on this page adds persistence to that pattern. - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) - [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) - [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration) # Control persistence hydration Use hydration tracking when your UI depends on saved state: display a loading message until storage has been read, and display an error instead of treating defaults as restored values. Hydration retrieves persisted state and merges it with the current store. For storage timing, see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data). ## 1. Track completion and errors Build on the [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create), [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist), and [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) setup in [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) by tracking hydration status separately from persisted data. The outer `onRehydrateStorage` callback runs before hydration. Its returned callback receives the restored state on success, or an error on failure. Keep the loading and error state in a separate, non-persisted store so it is not restored from storage itself. This also lets the callbacks update status during synchronous store initialization without updating the store being constructed. Add these files to your React browser project with `zustand` installed. The sample follows the authors' callback pattern and uses separate selectors for each value your components need. ```ts title="store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' type HydrationState = { ready: boolean error: string | null } export const useHydrationStore = create()(() => ({ ready: false, error: null, })) type BearStore = { bears: number addABear: () => void } export const useBearStore = create()( persist( (set) => ({ bears: 0, addABear: () => set((state) => ({ bears: state.bears + 1 })), }), { name: 'hydration-bears', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ bears: state.bears }), skipHydration: false, onRehydrateStorage: () => { useHydrationStore.setState({ ready: false, error: null }) return (_state, error) => { if (error !== undefined) { useHydrationStore.setState({ ready: false, error: error instanceof Error ? error.message : String(error), }) } else { useHydrationStore.setState({ ready: true, error: null }) } } }, }, ), ) ``` Select the status before displaying the counter. The counter and its button appear after successful hydration; a storage read or JSON parsing failure displays an alert instead. ```tsx title="App.tsx" import { useBearStore, useHydrationStore } from './store' export default function App() { const ready = useHydrationStore((state) => state.ready) const error = useHydrationStore((state) => state.error) const bears = useBearStore((state) => state.bears) const addABear = useBearStore((state) => state.addABear) if (error !== null) { return

Could not restore saved bears: {error}

} if (!ready) { return

Loading saved bears…

} return (

Bears: {bears}

) } ``` Mount the component in your project's root element: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import App from './App' createRoot(document.getElementById('root')!).render() ``` Click **Add a bear**, then reload the page. The counter displays the saved count after hydration. With synchronous `localStorage`, hydration can finish before the first render, so you may not see the loading message. With no saved entry, successful hydration keeps the initial count of zero. ## 2. Defer hydration until mount To control when the first hydration starts, change `skipHydration` to `true` in `store.ts`. This suppresses hydration during store initialization. Replace `App.tsx` with the following component to trigger it explicitly after mount, following the authors' `useEffect` pattern: ```tsx title="App.tsx" import { useEffect } from 'react' import { useBearStore, useHydrationStore } from './store' export default function App() { const ready = useHydrationStore((state) => state.ready) const error = useHydrationStore((state) => state.error) const bears = useBearStore((state) => state.bears) const addABear = useBearStore((state) => state.addABear) useEffect(() => { void useBearStore.persist.rehydrate() }, []) if (error !== null) { return

Could not restore saved bears: {error}

} if (!ready) { return

Loading saved bears…

} return (

Bears: {bears}

) } ``` The initial render displays the loading message. The effect calls `rehydrate()`, and the same callbacks reveal the restored counter or the error alert. The public return type of `rehydrate()` is `Promise | void`; use `await` when subsequent code needs to wait for an asynchronous hydration attempt, but use the callback's error argument to determine whether it succeeded. ## Options that matter In this table, `S` is your store state; the sample persists only `{ bears: number }`. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Required | Identifies the storage entry; use a unique key per store. | | `storage` | [`PersistStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persiststorage)`<{ bears: number }>` | `createJSONStorage(() => window.localStorage)` | Reads and writes persisted state. | | `partialize` | `(state: S) => { bears: number }` | Identity function | Filters state before writing; the sample saves only `bears`. | | `onRehydrateStorage` | `(state: S) => ((state?: S, error?: unknown) => void) \| void` | Not set | Runs logic before hydration and after success or failure. | | `skipHydration` | `boolean` | `false` | Disables the initial automatic hydration when `true`. | ## Pitfalls - Do not infer readiness from the counter's default value. Select an explicit status, as above. For asynchronous storage timing, see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data). - `hasHydrated()` is a non-reactive getter, not a subscription. Calling it during render does not subscribe the component to hydration status. The sample uses a store subscription instead. - Do not rely on a rejected `rehydrate()` promise or `onFinishHydration` to report errors: the middleware catches hydration errors and passes them to the returned `onRehydrateStorage` callback. `onFinishHydration` runs on the success path. - Deferring hydration does not replace per-request store setup in server-rendered applications. See [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). ## Live demo and related reading For a running example of persistence using another storage source, open the authors' [URL-hash persistence demo](https://stackblitz.com/edit/vitejs-vite-9vg24prg). It demonstrates URL-backed storage rather than the loading and error UI above; [Connect state to the URL](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/connect-state-to-the-url) explains that integration. - [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) — choose storage and decide which fields to save. - [Migrate persisted state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/migrate-persisted-state) — handle versions and nested-state merging. # Migrate persisted state Use versioning when you change the schema stored by [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist), such as moving flat coordinates into a nested `position` object. Add a custom `merge` when stored objects omit fields that your current store supplies as defaults. This browser example migrates version 0 data into version 1 and renders a red dot at `x: 40, y: 100`. The stored `y` survives migration; the missing `x` comes from the current store's nested defaults. ## 1. Version the store and migrate the old schema Keep the [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) setup from [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data); add `version`, `migrate`, and `merge` to handle the schema change below. In your React TypeScript project, install Zustand: ```bash npm install zustand ``` Create `position-store.ts`. The seed represents an older, partially persisted schema and only runs when this example's storage key is absent. Remove the seeding code from your application after testing the migration. ```ts title="position-store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' type PositionStore = { position: { x: number; y: number } moveRight: () => void } type PersistedPosition = { position: Partial } // Browser demonstration: seed an older schema before creating the store. const storageName = 'position-migration-example' if (localStorage.getItem(storageName) === null) { localStorage.setItem( storageName, JSON.stringify({ state: { y: 100 }, version: 0 }), ) } export const usePositionStore = create()( persist( (set) => ({ position: { x: 40, y: 40 }, moveRight: () => set((state) => ({ position: { ...state.position, x: state.position.x + 20 }, })), }), { name: storageName, storage: createJSONStorage(() => localStorage), version: 1, partialize: (state): PersistedPosition => ({ position: state.position, }), migrate: (persisted, version): PersistedPosition => { if ( version === 0 && typeof persisted === 'object' && persisted !== null ) { return { position: { ...('x' in persisted && typeof persisted.x === 'number' ? { x: persisted.x } : {}), ...('y' in persisted && typeof persisted.y === 'number' ? { y: persisted.y } : {}), }, } } // This example supports only the version 0 migration. return { position: {} } }, merge: (persisted, current) => { if ( typeof persisted !== 'object' || persisted === null || !('position' in persisted) || typeof persisted.position !== 'object' || persisted.position === null ) { return current } const position = persisted.position return { ...current, position: { ...current.position, ...('x' in position && typeof position.x === 'number' ? { x: position.x } : {}), ...('y' in position && typeof position.y === 'number' ? { y: position.y } : {}), }, } }, }, ), ) ``` `migrate` receives the stored state and its stored version, not the target version. Return data compatible with the current persisted schema; the callback can also return a promise. Here it moves the old `x` and `y` fields into `position`, without inventing values for missing coordinates. Hydration calls `merge` with the migrated data and the current state. After a successful migration, `persist` writes the merged, filtered state back to storage with version 1. `partialize` stores only `position`, leaving the action out of the stored data. ## 2. Preserve nested defaults and render the result The custom `merge` copies `current.position` first, then overlays valid stored coordinates. It returns the full store, including `moveRight`. The default shallow merge can erase unpersisted nested fields: a stored `{ position: { y: 100 } }` replaces the entire current `position` object. Merge that nested object explicitly, as above, or use a deep merge for a larger schema. Keep the current state as the base and let persisted fields override it. Mount a component that selects the position and action separately: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { usePositionStore } from './position-store' function App() { const position = usePositionStore((state) => state.position) const moveRight = usePositionStore((state) => state.moveRight) return (

x: {position.x}, y: {position.y}

) } createRoot(document.getElementById('root')!).render() ``` Reuse the `index.html` root element from [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start#3-mount-the-counter), with the module script's `src` set to `/main.tsx` to load this migration example. On the first run with an absent example key, the readout shows `x: 40, y: 100`, and the red dot appears at those offsets inside the bordered container. Click **Move right** to increase `x` by 20. Reload to see the saved position restored; version 1 data goes through `merge` without running `migrate` again. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Required | Sets the unique storage key. Keep the key when changing the schema so migration can read the old data. | | `version` | `number` | `0` | Sets the version written with persisted state. A different numeric stored version triggers migration. | | `migrate` | `(persistedState: unknown, version: number) => PersistedState \| Promise` | Not provided | Converts mismatched-version data to the current persisted schema. Without it, mismatched-version data is not used. | | `merge` | `(persistedState: unknown, currentState: State) => State` | `{ ...currentState, ...persistedState }` | Combines hydrated data with the current store. Supply a nested merge to retain omitted defaults. | | `partialize` | `(state: State) => PersistedState` | `(state) => state` | Filters what is written to storage. | In the table, `State` and `PersistedState` denote your store and persisted-data types. ## Check the migration boundary - Increment `version` for a breaking storage-schema change and handle each supported old version in `migrate`. The example handles version 0; other mismatched versions return an empty position, so the merge retains current defaults. - Migration runs only when the stored version is a number different from the configured version. A missing version does not trigger it. The custom merge still handles same-version partially persisted objects. - These files run in the browser and use synchronous `localStorage`. For asynchronous storage and initial-render behavior, see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data). For manual hydration, see [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration). ## Related - [Update nested state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-nested-state) — change nested state after hydration. - [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) — type middleware composition. # Debug store updates Use [`devtools`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#devtools) to inspect state changes in Redux DevTools without adding Redux or a reducer. Name the connection and each update so you can find the action that changed your state. ## 1. Set up browser debugging In your React TypeScript project, install Zustand and the extension library: ```bash npm install zustand @redux-devtools/extension ``` Install the [Redux DevTools browser extension](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) too. The npm package does not replace the browser extension: the middleware connects through the extension exposed on `window`. ## 2. Name the store and its updates Wrap the state creator with `devtools` inside [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create). `devtools` returns a state creator; `create` returns the hook your components use to select state and actions. No provider is needed. This browser entry point mounts two controls and a bear count. Use it with an HTML element whose id is `root`. ```tsx title="src/main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { devtools } from 'zustand/middleware' type BearStore = { bears: number addBears: (by: number) => void addOne: () => void } const useBearStore = create()( devtools( (set) => ({ bears: 0, addBears: (by) => set( (state) => ({ bears: state.bears + by }), undefined, { type: 'bear/addBears', by }, ), addOne: () => set((state) => ({ bears: state.bears + 1 })), }), { name: 'BearStore', anonymousActionType: 'bear/unnamed', enabled: true, }, ), ) function App() { const bears = useBearStore((state) => state.bears) const addBears = useBearStore((state) => state.addBears) const addOne = useBearStore((state) => state.addOne) return (

{bears} bears

) } const container = document.getElementById('root')! createRoot(container).render( , ) ``` ![The initial bear count and two buttons let you trigger the updates to inspect in DevTools.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/6550c8722dc8602a84832e7674494f10.png) Pass the action label as the third argument to `set`. A string supplies the action type; an object supplies `type` and payload fields. Keep the second argument `undefined` to preserve the default replacement logic. ## 3. Inspect the result Open Redux DevTools and select the `BearStore` connection. Click **Add two bears**: the page shows `2 bears`, and DevTools records `bear/addBears` with the payload field `by: 2` and the updated `bears` state. Click **Add one bear** next: the page shows `3 bears`, and DevTools records `bear/unnamed` because that update has no explicit action label. You can also jump to an earlier state in DevTools. The middleware applies that state to the store, so the selected bear count on the page changes with it. ## Options that matter Pass these options as the second argument to `devtools`. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Not supplied | Names the DevTools connection. Use different names to separate connections. | | `store` | `string` | Not supplied | Identifies a store within a shared named connection. Its state appears under this key, and its action types get a `store/` prefix. | | `anonymousActionType` | `string` | Inferred caller name, or `'anonymous'` if unavailable | Supplies the label for updates without an explicit action type. A nonempty value takes precedence over inference. | | `enabled` | `boolean` | Enabled outside production mode; disabled in production mode | Enables or disables the integration for this store. | The sample sets `enabled: true` explicitly, so it attempts to connect even in production. For a production build that must not connect, set `enabled: false`. Omit the option to use the middleware's mode-based default. Disabling the integration does not disable state updates or your React controls. To group multiple stores, give each the same `name` and a distinct `store` identifier. Without `store`, each middleware instance opens its own connection. ## Pitfalls - If no connection appears, check that the browser extension is available and that `enabled` is not `false`. Without the extension, the store still works, but no debugging connection is created. - DevTools displays one connection at a time. Use its store selector to choose another connection rather than assuming the other store is missing. - Use explicit third-argument labels when you need stable action names. Updates without them use `anonymousActionType`, caller-name inference, or the `'anonymous'` fallback. - For middleware composition and slice typing, follow [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware). ## Live demo and related pages Try the [live Zustand counter demo](https://zustand-demo.pmnd.rs/) for the hook-and-action interaction. The sample above adds named DevTools updates to that pattern. - [Update state with actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-state-with-actions) - [Split a store into slices](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/split-a-store-into-slices) - [Store scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle) # Update nested state Use immutable copies when an action changes a nested object, `Map`, or `Set`. For deeply nested objects, use the shipped Immer middleware to write the update with draft mutation syntax instead. The examples below render a counter or a collection summary in React, so you can see each update without losing sibling values. ## 1. Copy each level of a nested object Create a store with [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and put the update action beside its state. Use a functional `set` update to read the current value. Copy each object along the path to the changed field, keeping its other fields with object spread. You do not need to spread the top-level state: `set` merges the returned object at that level. In your React TypeScript project, use this as `src/main.tsx`. The page starts at **Forest: 0 bears**. Click **Add bear** to increment the count; the title and unit stay visible. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CounterStore = { deep: { title: string nested: { unit: string obj: { count: number; label: string } } } increment: () => void } const useCounterStore = create()((set) => ({ deep: { title: 'Forest', nested: { unit: 'bears', obj: { count: 0, label: 'Add bear' } }, }, increment: () => set((state) => ({ deep: { ...state.deep, nested: { ...state.deep.nested, obj: { ...state.deep.nested.obj, count: state.deep.nested.obj.count + 1, }, }, }, })), })) function App() { const deep = useCounterStore((state) => state.deep) const increment = useCounterStore((state) => state.increment) return (

{deep.title}: {deep.nested.obj.count} {deep.nested.unit}

) } createRoot(document.getElementById('root')!).render() ``` ![The counter starts at Forest: 0 bears, with an Add bear button below it.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/33b5eb3cecccc82609f3e2f486a7fa19.png) The update creates new `deep`, `nested`, and `obj` objects. Each spread preserves sibling fields at that level; the top-level merge preserves the `increment` action. Copy the changed path rather than rebuilding unrelated parts of the store. ## 2. Use an Immer draft instead of nested spreads Wrap the state creator in [`immer`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware-immer#immer) from `zustand/middleware/immer`. The middleware passes function updates through Immer's `produce`, so you can change a draft inside `set` while keeping the update immutable. It takes the state creator, not a configuration object. Install Immer as a direct dependency alongside Zustand: ```bash npm install zustand immer ``` Replace `src/main.tsx` with this alternative. It renders the same counter and preserves the same sibling values, but the action changes only the draft's count. Keep the extra parentheses in `create()(...)` when using middleware with an explicit state type. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' type CounterStore = { deep: { title: string nested: { unit: string obj: { count: number; label: string } } } increment: () => void } const useCounterStore = create()( immer((set) => ({ deep: { title: 'Forest', nested: { unit: 'bears', obj: { count: 0, label: 'Add bear' } }, }, increment: () => set((state) => { state.deep.nested.obj.count += 1 }), })), ) function App() { const deep = useCounterStore((state) => state.deep) const increment = useCounterStore((state) => state.increment) return (

{deep.title}: {deep.nested.obj.count} {deep.nested.unit}

) } createRoot(document.getElementById('root')!).render() ``` ![The Immer counter shows the same Forest title, zero count, and Add bear button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/33b5eb3cecccc82609f3e2f486a7fa19.png) ### Pitfall: class instances need Immer support If your state contains class instances, mark them with `[immerable] = true` as described in [Immer's class documentation](https://immerjs.github.io/immer/complex-objects), and follow [Immer's rules](https://immerjs.github.io/immer/pitfalls). Without that marker, Immer mutates the instance without proxying it, which also changes the current state. If the current and next state are equal, Zustand skips subscriptions. ## 3. Copy mutable collections before changing them For collections, create a new `Map` or `Set` from the current collection, then modify that copy. The following standalone alternative uses immutable copies without Immer. It starts with one bear and one selected animal. Click **Add bear** to increase the stored bear count, or **Select fox** to add another selected animal while keeping the bear selected. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CollectionStore = { counts: Map selected: Set addBear: () => void selectFox: () => void } const useCollectionStore = create()((set) => ({ counts: new Map([['bear', 1]]), selected: new Set(['bear']), addBear: () => set((state) => ({ counts: new Map(state.counts).set( 'bear', (state.counts.get('bear') ?? 0) + 1, ), })), selectFox: () => set((state) => ({ selected: new Set(state.selected).add('fox'), })), })) function App() { const counts = useCollectionStore((state) => state.counts) const selected = useCollectionStore((state) => state.selected) const addBear = useCollectionStore((state) => state.addBear) const selectFox = useCollectionStore((state) => state.selectFox) return (

Bears: {counts.get('bear') ?? 0}

Selected: {Array.from(selected).join(', ')}

) } createRoot(document.getElementById('root')!).render() ``` ![The collection summary starts with one bear and bear selected, alongside Add bear and Select fox buttons.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/d50c0f32e713db99050a20f8911000f1.png) The changed collection gets a new reference, and existing entries survive the copy. To delete an entry, copy the collection into a local variable, call `delete` on the copy, and return it from the updater. Give collections explicit type arguments, as above, when initializing them empty too. For the reference-equality rules behind these updates, see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions). For selecting derived collection values, see [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). ## Live demos - [Nested update approaches](https://stackblitz.com/edit/vitejs-vite-j6bjdygu) - [Immer middleware](https://stackblitz.com/edit/vitejs-vite-3sgc4ejy) - [Map and Set updates](https://stackblitz.com/edit/vitejs-vite-5cu5ddvx) ## Related - [Update state with actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-state-with-actions) - [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) - [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start) # Split a store into slices Use slices when your global store grows too large to maintain in one state creator. Keep state and actions together in each slice, then compose the slice creators into one store. This example renders bear and fish counts, calls actions across slices, and adds persistence at the combined-store boundary. ## 1. Define typed slice creators For slices, use [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator) with the combined `BoundState` as its first type argument and the individual slice's return type as its fourth; see [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works) for state creators and their `set` and `get` arguments. Create this file in your React TypeScript project: ```ts title="slices.ts" import type { StateCreator } from 'zustand' export type BearSlice = { bears: number addBear: () => void eatFish: () => void } export type FishSlice = { fishes: number addFish: () => void } export type SharedSlice = { addBoth: () => void getBoth: () => number } export type BoundState = BearSlice & FishSlice & SharedSlice export const createBearSlice: StateCreator = ( set, ) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) export const createFishSlice: StateCreator = ( set, ) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) export const createSharedSlice: StateCreator = ( _set, get, ) => ({ addBoth: () => { get().addBear() get().addFish() }, getBoth: () => get().bears + get().fishes, }) ``` `eatFish()` updates a field owned by the fish slice. `addBoth()` reads the combined store with `get()` and calls both slices' actions. Each action returns `void`; `getBoth()` returns the current sum as a `number`. The two calls inside `addBoth()` perform two store updates, not one atomic update. ## 2. Compose one hook and render its state Use [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) with `create()(...)`. Pass the same `set`, `get`, and store arguments to every slice creator, then spread their results into one state object. The result is a bound store hook, not a separate store for each slice. ```ts title="store.ts" import { create } from 'zustand' import { createBearSlice, createFishSlice, createSharedSlice, type BoundState, } from './slices' export const useBoundStore = create()((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), ...createSharedSlice(...args), })) ``` Select each count and action from that hook. This browser entry point mounts the component into a sized container: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { useBoundStore } from './store' function App() { const bears = useBoundStore((state) => state.bears) const fishes = useBoundStore((state) => state.fishes) const total = useBoundStore((state) => state.bears + state.fishes) const addBear = useBoundStore((state) => state.addBear) const addFish = useBoundStore((state) => state.addFish) const eatFish = useBoundStore((state) => state.eatFish) const addBoth = useBoundStore((state) => state.addBoth) return (

Bear and fish store

Number of bears: {bears}

Number of fishes: {fishes}

Total: {total}

) } const container = document.createElement('div') container.style.height = '400px' document.body.appendChild(container) createRoot(container).render() ``` The initial counts and total are zero. Click **Add both** to see one bear, one fish, and a total of two. Click **Eat a fish** to see the fish count fall to zero while the bear count stays at one. The button disables at zero; the store action itself does not enforce that limit. ## 3. Apply middleware to the combined store Replace `store.ts` with this version to persist the counts using the shipped [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist) middleware. Keep the slice creators and `main.tsx` unchanged. ```ts title="store.ts" import { create } from 'zustand' import { persist } from 'zustand/middleware' import { createBearSlice, createFishSlice, createSharedSlice, type BoundState, } from './slices' export const useBoundStore = create()( persist( (...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), ...createSharedSlice(...args), }), { name: 'bear-fish-slices', partialize: (state) => ({ bears: state.bears, fishes: state.fishes }), }, ), ) ``` In the browser, with `localStorage` available, change the counts and reload to see them restored. `partialize` saves only the two counts; the slice creators supply the actions again when the store is created. ### Persistence options The storage adapter uses [`PersistStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persiststorage); [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) supplies JSON serialization for a storage engine. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Required | Sets the unique storage key for this combined store. | | `storage` | `PersistStorage \| undefined` | `createJSONStorage(() => window.localStorage)` | Selects the storage adapter. | | `partialize` | `(state: S) => PersistedState` | `(state) => state` | Filters the state before saving it. | For storage and hydration configuration, see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data). ## Pitfalls - Keep slice creators unwrapped and put middleware around the combined creator, as above. See [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) for middleware composition rules. When using [`devtools`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#devtools), annotate each slice's incoming mutator tuple as `[['zustand/devtools', never]]` instead of `[]`. - Keep `get()` calls inside actions such as `addBoth()` and `getBoth()`. For initialization-time reads, see [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state). - Keep slice field names distinct. Object spreading makes a later slice's value overwrite an earlier value with the same key. ## Related - [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works) — the single-store model and colocated actions. - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) — updates and shallow merging. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) — selecting values from the combined hook. # Initialize scoped stores Use a vanilla store with React context when you need dependency injection or initial state from component props. Each provider supplies an independent store; consumers beneath that provider share it. The store exists independently of React: [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore) returns the store API, and [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore) connects a React component to it. Context carries the store instance, not a bound hook. The example below renders two bear counters initialized to 2 and 5. Clicking **Add bear** changes only the counter in that provider's scope. Run it in a browser React TypeScript project with Zustand installed: ```bash npm install zustand ``` ## 1. Create a store factory Accept initial props in a factory rather than creating one global instance. Each call returns a new [`StoreApi`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#storeapi) with its own state and listeners. Merge the initial props over your application defaults, then define the actions. ```ts title="bear-store.ts" import { createStore, type StoreApi } from 'zustand' export type BearProps = { bears: number } export type BearState = BearProps & { addBear: () => void } export function createBearStore( initProps: Partial = {}, ): StoreApi { const defaults: BearProps = { bears: 0 } return createStore()((set) => ({ ...defaults, ...initProps, addBear: () => set((state) => ({ bears: state.bears + 1 })), })) } ``` The functional update reads the previous count and returns the next count. The merging update preserves `addBear` in the store. ## 2. Provide a stable instance and a typed hook Keep the store in a lazy `useState` initializer, following the provider-wrapper pattern. The provider retains that instance across re-renders instead of creating a new store on every render. The consumer hook accepts a selector and returns its inferred result type. It reads the context, rejects a missing provider, and delegates the subscription to `useStore`. ```tsx title="bear-context.tsx" import { createContext, useContext, useState, type PropsWithChildren, } from 'react' import { useStore, type StoreApi } from 'zustand' import { createBearStore, type BearProps, type BearState } from './bear-store' const BearContext = createContext | null>(null) type BearProviderProps = PropsWithChildren> export function BearProvider({ children, bears }: BearProviderProps) { const [store] = useState(() => createBearStore({ bears: bears ?? 0 })) return ( {children} ) } export function useBearContext(selector: (state: BearState) => T): T { const store = useContext(BearContext) if (store === null) { throw new Error('Missing BearProvider in the tree') } return useStore(store, selector) } function BearCount() { const bears = useBearContext((state) => state.bears) const addBear = useBearContext((state) => state.addBear) return (

{bears} Bears.

) } export default function BearScopeExample() { return (
) } ``` ![The two independent scopes start with 2 Bears and 5 Bears, each with its own Add bear button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/20043d062e53118ca573af347f2a9733.png) ## 3. Render independent scopes Select the count and action separately. Both consumers use the same hook, but each reads the store supplied by its nearest provider. ```tsx title="App.tsx" import { BearProvider, useBearContext } from './bear-context' function BearCounter({ label }: { label: string }) { const bears = useBearContext((state) => state.bears) const addBear = useBearContext((state) => state.addBear) return (

{label}

{bears} Bears.

) } export default function App() { return (
) } ``` Mount `App` in your project's root element. This entry point expects an element with `id="root"` in your HTML: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import App from './App' const container = document.getElementById('root') if (container === null) { throw new Error('Missing root element') } createRoot(container).render() ``` You see **First scope** with `2 Bears.` and **Second scope** with `5 Bears.` Click the first scope's button: its count becomes 3, while the second stays at 5. Put additional consumers inside either provider to share that provider's store. ## Inputs that matter `useStore` takes the store instance and an optional selector; it has no options object. | Option | Type | Default | What it does | | --- | --- | --- | --- | | Store argument | `StoreApi` in this example | Required | Chooses the instance to subscribe to. | | Selector argument | `(state: BearState) => T` in the consumer hook | Identity selector in `useStore`; required by `useBearContext` | Returns the value the component reads. | The example's application-defined `bears` prop supplies the initial count and defaults to 0 when omitted. It is not a Zustand configuration option. ## Pitfalls - Treat provider props as initialization, not synchronization. The lazy initializer captures the initial props; later prop changes do not update the retained store. Use a store action for subsequent updates. - Keep each consumer under `BearProvider`. The custom hook throws when the context contains `null`. - Do not create the store directly in the provider's render body. Keep the lazy initializer so re-renders retain the current count. - For selectors that derive objects or arrays, see [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). - For server rendering and request-local initialization, see [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). ## Live demo Try the [Zustand live demo](https://zustand-demo.pmnd.rs/) to see a counter driven by a store action. The scoped example above adds context and a store factory so separate counters do not share one global store. ## Related - [Store scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle) - [Update state with actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-state-with-actions) - [Test stores and components](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/test-stores-and-components) # Next.js store setup Use a store factory and a React context provider when you need request-isolated state in Next.js. Initialize the server and client stores with the same data so their first render matches. This example renders `Count: 10`; the buttons increment and decrement the count in the mounted client component. Unlike the bound hook returned by [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create), a vanilla store exists independently of React. Create it with [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore), then connect client components with [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore). Context supplies the store for this part of the component tree. ## 1. Create a store factory In your existing Next.js TypeScript project, install [Zustand](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand): ```bash npm install zustand ``` Place the three shared files below beside your route's page file (`src/app/page.tsx` or `src/pages/index.tsx`). Export a factory rather than a store instance. Each call returns a new store containing the initial count and its actions. `CounterState` and `CounterActions` describe your application data and operations; their intersection gives the store both. ```ts title="counter-store.ts" import { createStore } from 'zustand/vanilla' export type CounterState = { count: number } export type CounterActions = { decrementCount: () => void incrementCount: () => void } export type CounterStore = CounterState & CounterActions export const defaultInitState: CounterState = { count: 0 } export const createCounterStore = ( initState: CounterState = defaultInitState, ) => createStore()((set) => ({ ...initState, decrementCount: () => set((state) => ({ count: state.count - 1 })), incrementCount: () => set((state) => ({ count: state.count + 1 })), })) ``` ## 2. Provide a stable store to client components Use a lazy `useState` initializer to retain the provided store across re-renders. The context holds the store API, not a bound hook. Derive its type from the factory's return value. The custom hook returns the selector's result and subscribes the component to it. It throws a descriptive error if you use it outside the provider. `CounterExample` composes the provider with the client counter defined below, rendering the count and both buttons when you mount ``. ```tsx title="counter-store-provider.tsx" 'use client' import { createContext, useContext, useState, type ReactNode } from 'react' import { useStore } from 'zustand' import { Counter } from './counter' import { createCounterStore, type CounterState, type CounterStore, } from './counter-store' type CounterStoreApi = ReturnType const CounterStoreContext = createContext(undefined) type CounterStoreProviderProps = { children: ReactNode initialState: CounterState } export function CounterStoreProvider({ children, initialState, }: CounterStoreProviderProps) { const [store] = useState(() => createCounterStore(initialState)) return ( {children} ) } export function useCounterStore(selector: (state: CounterStore) => T): T { const store = useContext(CounterStoreContext) if (!store) { throw new Error('useCounterStore must be used within CounterStoreProvider') } return useStore(store, selector) } export default function CounterExample() { return ( ) } ``` ![The counter displays Count: 10 with Increment Count and Decrement Count buttons.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/d786cdcb323f62f900b667e4302d2361.png) Create a client component that selects the count and each action separately. After hydration, clicking **Increment Count** adds one to the displayed count; **Decrement Count** subtracts one. ```tsx title="counter.tsx" 'use client' import { useCounterStore } from './counter-store-provider' export function Counter() { const count = useCounterStore((state) => state.count) const incrementCount = useCounterStore((state) => state.incrementCount) const decrementCount = useCounterStore((state) => state.decrementCount) return (

Count: {count}

) } ``` ## 3. Supply matching initial state Choose the example for your router. Both mount the provider at page level, following the per-route setup. The initial count is explicit application data, not a value computed independently in the browser. ### App Router Keep your existing root layout and add this page. The Server Component passes only initial data to the client provider; it does not read or write a Zustand store. ```tsx title="src/app/page.tsx" import { Counter } from './counter' import { CounterStoreProvider } from './counter-store-provider' import type { CounterState } from './counter-store' export default function Home() { const initialState: CounterState = { count: 10 } return ( ) } ``` ### Pages Router Pass initial data through page props. Here, `getServerSideProps` supplies the initial count; the page passes that same value to the provider for server rendering and client hydration. Use this page-level provider without an additional counter provider in `_app.tsx`. ```tsx title="src/pages/index.tsx" import { Counter } from './counter' import { CounterStoreProvider } from './counter-store-provider' import type { CounterState } from './counter-store' type HomeProps = { initialState: CounterState } export const getServerSideProps = async (): Promise<{ props: HomeProps }> => { return { props: { initialState: { count: 10 } } } } export default function Home({ initialState }: HomeProps) { return ( ) } ``` Open the home page in your running Next.js app. It renders `Count: 10` with both buttons, then updates the displayed count when you click them. ## Initialization and scope `createStore` takes a state creator, not Next.js-specific configuration options. In this example, these are your factory and provider inputs: | Input | Type | Default | What it does | | --- | --- | --- | --- | | Factory `initState` | `CounterState` | `{ count: 0 }` | Supplies the count when creating a store. | | Provider `initialState` | `CounterState` | Required | Supplies data to the factory on provider initialization. | Keep the provided store stable: `initialState` initializes it, rather than synchronizing it on every render. Do not recreate the store in the provider's render body. Place the provider at page level when you need a per-route store. If you do not need per-route scope, the setup also supports a provider in the App Router's layout or the Pages Router's `_app.tsx`. See [Store scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle) for choosing the store's lifetime. ## Pitfalls - **Do not export a server-side singleton store.** A Next.js server handles multiple requests, so a global store can share state between them. Export the factory and create the store inside the provider instead. - **Match the initial data.** Different server and client output causes hydration errors. Pass the same initial data to both stores; do not independently calculate a time-dependent or browser-only value during initialization. - **Keep Server Components out of the store.** Do not read or write Zustand state from React Server Components. Pass initial data into the client provider and access the store through client components. `useStore` uses `getInitialState()` for its server snapshot and `getState()` for its current client snapshot. Matching the two stores' initialization is therefore part of making their first render agree. ## Related - [Initialize scoped stores](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/initialize-scoped-stores) — initialize a vanilla store from component props. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) — choose what each component subscribes to. - [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration) — coordinate loading persisted state with rendering. # Subscribe outside React Use a selected subscription to update a non-React consumer only when its chosen value changes; see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) for creating a store with [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and reading or updating its state. This browser example shows the same counter in a React heading and an external consumer's output. Clicking **Increment outside React** updates both. Clicking **Toggle snout** changes another field without notifying the counter's selected subscription. ## 1. Enable selected subscriptions In your React TypeScript project, install Zustand: ```bash npm install zustand ``` Wrap the state creator with [`subscribeWithSelector`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#subscribewithselector). This shipped middleware adds `subscribe(selector, listener, options)` while preserving the whole-store subscription signature. ```ts title="store.ts" import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' type CounterState = { count: number snout: boolean toggleSnout: () => void } export const useCounterStore = create()( subscribeWithSelector((set) => ({ count: 0, snout: true, toggleSnout: () => set((state) => ({ snout: !state.snout })), })), ) ``` The result is a React hook with store methods attached. Keep the inferred return type so the middleware's selected `subscribe()` overload remains available. ## 2. Read, update and subscribe without calling a hook `getState()` returns the current state without subscribing. Use `setState()` for an external update; the functional form reads the current count, and the default shallow merge preserves the other fields and action. The listener receives the new and previous selected values. `fireImmediately: true` also invokes it during setup, with the current value as both arguments. Return the unsubscribe function to whoever owns the consumer's lifetime. ```ts title="counter-consumer.ts" import { useCounterStore } from './store' export function incrementOutsideReact(): void { useCounterStore.setState((state) => ({ count: state.count + 1 })) } export function attachCounterOutput(output: HTMLOutputElement): () => void { output.textContent = `Count: ${useCounterStore.getState().count}` return useCounterStore.subscribe( (state) => state.count, (count, previousCount) => { output.textContent = `Count: ${count} (previous: ${previousCount})` }, { fireImmediately: true }, ) } ``` This module runs in the browser and never calls the React hook. The selected listener runs synchronously when the count changes; changing only `snout` leaves its output untouched. For all state changes rather than one selected value, pass only a listener to `subscribe()`. That listener receives the complete current state and previous state, and the call still returns an unsubscribe function. ## 3. Mount the consumer and clean up on unmount Give the external consumer its own output element. React selects `count` and `snout` through the hook, while the effect connects the external consumer after the element mounts. Returning its unsubscribe function from the effect disconnects that listener on unmount. ```tsx title="App.tsx" import { useEffect, useRef } from 'react' import { attachCounterOutput, incrementOutsideReact } from './counter-consumer' import { useCounterStore } from './store' export default function App() { const count = useCounterStore((state) => state.count) const snout = useCounterStore((state) => state.snout) const toggleSnout = useCounterStore((state) => state.toggleSnout) const outputRef = useRef(null) useEffect(() => { const output = outputRef.current if (!output) return return attachCounterOutput(output) }, []) return (

React count: {count}

External consumer:

Snout: {snout ? 'on' : 'off'}

) } ``` Mount it in your project's entry point, with a `
` in the HTML: ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import App from './App' const container = document.getElementById('root')! createRoot(container).render() ``` The heading starts at `React count: 0`, and the external output starts at `Count: 0 (previous: 0)`. Increment once to see the heading change to `1` and the output to `Count: 1 (previous: 0)`. Toggle snout to see its text change while the counter output stays the same. ## Subscription options Pass these options as the third argument of the selected `subscribe()` overload: | Option | Type | Default | What it does | | --- | --- | --- | --- | | `equalityFn` | `(a: U, b: U) => boolean` | `Object.is` | Compares selected values. The listener runs only when the comparison returns `false`. `U` is the selector's result type. | | `fireImmediately` | `boolean` | No immediate call | Invokes the listener during setup with the current selected value as both arguments. | ## Pitfalls - Without `fireImmediately`, subscribing registers a listener but does not render the initial value. Read with `getState()` and initialize the consumer yourself, or enable the option. - Unsubscribe when the consumer tears down, not immediately after subscribing. Unsubscribing removes that listener; it does not remove the store. - Middleware that modifies the state creator's `set` or `get` does not necessarily modify the public `setState()` or `getState()` methods. Use your store's actions when you need their update behavior. - For immutable updates and nested merging, see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions). For server-side store ownership, see [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). ## Live demo and related guides Explore the authors' [live Zustand demo](https://zustand-demo.pmnd.rs/) for the counter hook in action. Its [source](https://github.com/pmndrs/zustand/blob/main/examples/demo/src/App.jsx) shows the React counter; the example above adds an external selected subscription. Apply the same external subscription and cleanup pattern to a store created with [`createStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#createstore); see [How Zustand works](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/how-zustand-works) for standalone stores and connecting them to React with [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore). As an alternative entry point, this sample mounts a React counter connected to a vanilla store. Clicking the button updates the store through `setState()` and the selected count appears in the heading. ```tsx title="main.tsx" import { createRoot } from 'react-dom/client' import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' type CounterState = { count: number } const counterStore = createStore()(() => ({ count: 0 })) function incrementOutsideReact(): void { counterStore.setState((state) => ({ count: state.count + 1 })) } function VanillaCounter() { const count = useStore(counterStore, (state) => state.count) return (

React count: {count}

) } const container = document.getElementById('root')! createRoot(container).render() ``` ![The React heading displays the vanilla store's initial count of 0 above the external increment button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/b32011c738aa03452afeb766a744ec2a.png) - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) - [Store scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle) # Reset store state Reset a store after logout or clearing a session. Use [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) to keep state and actions together, then reset through `getInitialState()`. The examples below restore the displayed counts and leave the increment actions available for subsequent updates. ## 1. Reset one store Add a `reset` action that calls `set(store.getInitialState())`. `getInitialState()` returns the state created by the initializer, including its actions; it does not capture the most recent update. In a React browser project with Zustand installed, use this as `src/main.tsx` and provide an element with `id="root"` in your HTML: ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type BearState = { bears: number increase: (by: number) => void reset: () => void } const useBearStore = create()((set, _get, store) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), reset: () => set(store.getInitialState()), })) function ResetZoo() { const bears = useBearStore((state) => state.bears) const increase = useBearStore((state) => state.increase) const reset = useBearStore((state) => state.reset) return (

{bears} bears

) } createRoot(document.getElementById('root')!).render() ``` ![The bear count starts at zero, with buttons to increase it and reset the store.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/5aa8f5a194102a3e6d1552884e035252.png) The component starts at **0 bears**. Click **Increase by 5**, then **Reset**: the count returns to zero. Click **Increase by 5** again to confirm the action still works. Each selector subscribes to one field of the store. If you keep a separate `initialState` object containing only data, `reset: () => set(initialState)` also works: `set` merges that object at one level, retaining the actions. Try the [basic reset demo](https://stackblitz.com/edit/zustand-how-to-reset-state-basic). ## 2. Reset several stores together Register a reset callback when you create each store. Use the library's [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator) type for the initializer rather than describing its signature yourself. This helper handles stores without middleware and returns the bound hook from `create`. Replace `src/main.tsx` with this example. Both stores are connected to the rendered component, so **Reset all stores** visibly restores both counts. ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client' import { create, type StateCreator } from 'zustand' const storeResetFns = new Set<() => void>() function createResettable(stateCreator: StateCreator) { const store = create()(stateCreator) storeResetFns.add(() => { store.setState(store.getInitialState(), true) }) return store } function resetAllStores() { storeResetFns.forEach((reset) => reset()) } type BearState = { bears: number addBear: () => void } type FishState = { fish: number addFish: () => void } const useBearStore = createResettable((set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), })) const useFishStore = createResettable((set) => ({ fish: 2, addFish: () => set((state) => ({ fish: state.fish + 1 })), })) function ResetZoo() { const bears = useBearStore((state) => state.bears) const addBear = useBearStore((state) => state.addBear) const fish = useFishStore((state) => state.fish) const addFish = useFishStore((state) => state.addFish) return (

{bears} bears, {fish} fish

) } createRoot(document.getElementById('root')!).render() ``` ![The display starts at zero bears and two fish, with separate add buttons and one button to reset both stores.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/edcece49082eb9df27406fc8fe2e269a.png) Click both add buttons, then **Reset all stores**. The display returns to **0 bears, 2 fish**. Both add buttons still work because each replacement supplies the complete initial state, including actions. The callbacks reset stores one after another; this is not a cross-store transaction. Only stores created through `createResettable` enter this registry. Keep this pattern for application-lifetime stores; for dynamically scoped stores, manage registrations alongside their [scope and lifecycle](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/store-scope-and-lifecycle). Try the [advanced reset demo](https://stackblitz.com/edit/zustand-how-to-reset-state-advanced). ## Reset options The second argument to `set` or `setState` controls how an object reset applies: | Option | Type | Default | What it does | | --- | --- | --- | --- | | `replace` | `false` or `true` | Omitted (`undefined`) | For object state, omission or `false` merges at one level; `true` replaces the state with the supplied complete state. | For replacement typing and complete-state requirements, see [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware). ## Pitfalls Do not call `get` synchronously while constructing the initial state. It returns `undefined` before that state exists, and accessing a field can throw even when TypeScript accepts the code. Read the store only after initialization—for example, inside an action called from a button. Likewise, keep the `getInitialState()` call inside the reset callback, as shown above. ## Related - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) explains merging updates. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) explains selecting fields for components. - [Test stores and components](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/test-stores-and-components) covers verifying store behavior. # Type stores and middleware Use this pattern to type state and actions, infer them from initial values, reuse the store's type, and compose middleware without losing inference. The React example renders a bear count; clicking **Increase by 1** updates that count. ## 1. Annotate state and actions with curried create In your React TypeScript project, pass your state-and-actions type to [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) using `create()(...)`. The extra `()` lets you annotate the state while TypeScript infers the middleware type parameters. Your store is a hook: select the state and actions each component needs. ```ts title="store.ts" import { create } from 'zustand' export type BearState = { bears: number increase: (by: number) => void } export const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) ``` TypeScript checks that the creator returns every field in `BearState`. It infers `by` as `number`, so you do not need to annotate the action parameter again. Render the hook's selected values in a component. `BearSummary` uses the same state type for its props rather than repeating the type of `bears`. ```tsx title="App.tsx" import { useBearStore, type BearState } from './store' function BearSummary({ bears }: Pick) { return

{bears} bears around

} export default function App() { const bears = useBearStore((state) => state.bears) const increase = useBearStore((state) => state.increase) return (
) } ``` Keep the HTML entry point and `main.tsx` from [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start#3-mount-the-counter); replace its `store.ts` and `App.tsx` with the files above to reuse `BearState` for component props in the mounted counter. Here, `BearState` also supplies the component prop type; see [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) for the bound hook and its store methods. ## 2. Infer state with combine and extract its type Replace `store.ts` with the following file; keep `App.tsx` and `main.tsx` unchanged. [`combine`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#combine) merges the initial state with the object returned by its additional state creator. It returns a state creator, not a store, so pass it to `create`. Use [`ExtractState`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#extractstate) to export the inferred state-and-actions type. ```ts title="store.ts" import { create, type ExtractState } from 'zustand' import { combine } from 'zustand/middleware' export const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), ) export type BearState = ExtractState ``` Use the non-curried `create(...)` here: `combine` creates the state, so TypeScript can infer its type. Annotate `by` because the initial state does not describe that action's parameter. `ExtractState` extracts the full state-and-actions type from a store's `getState()` method. Here, `BearState` contains both `bears: number` and `increase: (by: number) => void`. The existing `BearSummary` props still compile, and the mounted counter behaves the same way. You can reuse this extracted type in component props, utilities, and tests. ### Keep merging updates unless you intend to replace everything Passing `true` as the replacement flag replaces the entire state, including actions. Supply a complete state when replacing, or use the default merging update as the examples do. Be especially careful inside `combine`: its internal `get` type describes only the initial state, although the runtime state also includes the returned actions. A replacement containing only `bears` can compile there while deleting `increase`. Similarly, `Object.keys(get())` includes action keys despite the narrower type. The resulting hook's `getState()` type includes the full state and actions. ## 3. Compose middleware directly inside create Replace `store.ts` again to add [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist) and [`devtools`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#devtools). Keep the middleware nested immediately inside curried `create` so contextual inference works. Do not move the composition into an untyped wrapper function. ```ts title="store.ts" import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' export type BearState = { bears: number increase: (by: number) => void } export const useBearStore = create()( devtools( persist( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'typed-bear-storage', partialize: (state) => ({ bears: state.bears }), }, ), { name: 'Typed bears' }, ), ) ``` The same mounted component now saves the bear count to browser `localStorage`. With storage available, refreshing the page restores the saved count. `partialize` selects only `bears` for storage; TypeScript infers that persisted shape while the hook retains the full `BearState` type. Install the [Redux DevTools extension](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) to inspect the store's updates. Keep `devtools` outermost when composing it with middleware such as Immer. It adds a type parameter to `setState`; middleware that also modifies `setState` can otherwise lose that parameter. If you split the creator into slices, apply middleware to the combined store, not inside individual slices. Middleware inside slices can produce unexpected issues. See [Split a store into slices](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/split-a-store-into-slices) for that pattern. ### Options that matter here For a different JSON-backed storage, use the shipped [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) adapter. It returns [`PersistStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persiststorage) or `undefined` if obtaining storage throws. See [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) for storage configuration and hydration behavior. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `persist` → `name` | `string` | Required | Sets the storage key; use a unique name. | | `persist` → `partialize` | `(state: S) => PersistedState` | `(state) => state` | Selects the value to persist; the return value determines the persisted type. | | `persist` → `storage` | `PersistStorage \| undefined` | `createJSONStorage(() => window.localStorage)` | Chooses the storage used for persistence. | | `devtools` → `name` | `string` | Not specified | Names the DevTools connection. | If you extract a named creator, annotate it with [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator), not a hand-written function type. Its generic parameters describe the full state, incoming middleware mutators, outgoing mutators, and the returned slice. A plain `StateCreator` describes a creator without middleware mutators; keep middleware-specific mutator information when annotating creators that depend on it. ## Live demo and related tasks Try the [live Zustand counter demo](https://zustand-demo.pmnd.rs/) to see the hook-and-action interaction. The examples above follow the React starter's separate count and action selectors, with curried creation and middleware typing added here. - [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start) — set up the React integration. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) — choose what a component reads. - [Debug store updates](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/debug-store-updates) — name and inspect actions. - [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state) — initialize and reset state safely. # Connect state to the URL Use URL persistence when you want a copied link to restore store data. In this browser-only React example, clicking **Add a fish** updates the count and the URL hash. Opening the copied URL in a new tab restores that count. You can switch the same store to query parameters with a `localStorage` fallback. Keep the [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create), [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist), and [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) setup, but replace its storage with a URL-backed [`StateStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#statestorage) adapter; see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) for the persistence setup. ## 1. Create hash storage Add this file to your React project. `getItem` returns the stored string, or `null` when the key is absent. `setItem` updates one hash parameter, and `removeItem` deletes it; both preserve other hash parameters. ```ts title="hash-storage.ts" import type { StateStorage } from 'zustand/middleware' export const urlStorage: StateStorage = { getItem: (key) => { const params = new URLSearchParams(window.location.hash.slice(1)) return params.get(key) }, setItem: (key, value) => { const params = new URLSearchParams(window.location.hash.slice(1)) params.set(key, value) window.location.hash = params.toString() }, removeItem: (key) => { const params = new URLSearchParams(window.location.hash.slice(1)) params.delete(key) window.location.hash = params.toString() }, } ``` Treat the hash as a parameter list, not as a route or an element anchor. ## 2. Connect the adapter to a typed store Use `food-storage` as the URL parameter name and share only `fishes`; see [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) for typing `storage` with [`PersistStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persiststorage) and selecting persisted fields with `partialize`. ```ts title="fish-store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import { urlStorage } from './hash-storage' type FishData = { fishes: number } type FishStore = FishData & { addAFish: () => void } export const useFishStore = create()( persist( (set) => ({ fishes: 0, addAFish: () => set((state) => ({ fishes: state.fishes + 1 })), }), { name: 'food-storage', storage: createJSONStorage(() => urlStorage), partialize: (state) => ({ fishes: state.fishes }), version: 0, }, ), ) ``` Each action calls `set`, which updates the store and writes the selected data to storage. The JSON value has the persistence envelope `{"state":{"fishes":1},"version":0}`, not just `{"fishes":1}`. `URLSearchParams` encodes that string in the URL. ## 3. Mount the React counter Use separate selectors for the count and action. This entry point mounts the counter into your project's `root` element. ```tsx title="main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { useFishStore } from './fish-store' function FishCounter() { const fishes = useFishStore((state) => state.fishes) const addAFish = useFishStore((state) => state.addAFish) return (

Fish: {fishes}

Copy the address after adding fish to share this count.

) } const root = document.getElementById('root')! createRoot(root).render( , ) ``` With no stored value, the counter starts at zero. Click **Add a fish**: the count increases and the address gains a `food-storage` hash parameter. Copy the address and open it in a new tab. The synchronous adapter hydrates the store during initialization, restoring the count. Try the authors' [live hash demo](https://stackblitz.com/edit/vitejs-vite-9vg24prg). ## 4. Use query parameters instead For query persistence, add the following adapter and change the import in `fish-store.ts` from `./hash-storage` to `./query-storage`. Keep the store and React counter unchanged. This variant reads the named query parameter first and falls back to `localStorage` when that parameter is absent. Every update writes to both places. It uses `history.replaceState` to update the address in place without a refresh, preserving the path, other query parameters, and the hash. ```ts title="query-storage.ts" import type { StateStorage } from 'zustand/middleware' export const urlStorage: StateStorage = { getItem: (key) => { const url = new URL(window.location.href) return url.searchParams.get(key) ?? window.localStorage.getItem(key) }, setItem: (key, value) => { const url = new URL(window.location.href) url.searchParams.set(key, value) window.history.replaceState(window.history.state, '', url.toString()) window.localStorage.setItem(key, value) }, removeItem: (key) => { const url = new URL(window.location.href) url.searchParams.delete(key) window.history.replaceState(window.history.state, '', url.toString()) window.localStorage.removeItem(key) }, } ``` Click **Add a fish** to populate the query parameter, then copy the address. Opening that link restores its count even if the receiving browser has a different locally stored count. The authors' example conditionally writes to the URL only when query parameters already exist. This variant always populates the URL, so you can share state starting from an address with no query string. Try the authors' [live query demo](https://stackblitz.com/edit/vitejs-vite-hyc97ynf). ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | Required | Sets the storage key and therefore the URL parameter name. Use a unique name per store. | | `storage` | `PersistStorage` | `createJSONStorage(() => window.localStorage)` | Replaces browser local storage with your JSON-wrapped URL adapter. | | `partialize` | `(state: FishStore) => FishData` in this example | `(state) => state` | Selects the fields to persist and share. | | `version` | `number` | `0` | Adds a version to the persistence envelope. A differing stored numeric version needs a migration to be used. | ## Pitfalls - Do not parse or stringify JSON again inside these adapters. `createJSONStorage` already does both. Return `null` for a missing key, not an empty string that fails JSON parsing. - URL data is editable. `createJSONStorage` parses JSON but does not validate its shape. For production use, implement a validating `PersistStorage` before accepting untrusted shared state. - These adapters persist updates; they do not register listeners for subsequent URL navigation. Open the copied link as a new page to hydrate it. For explicit hydration control, see [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration). - Keep browser-only URL storage out of a shared server store. For server-rendered React applications, see [Next.js store setup](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/nextjs-store-setup). ## Related - [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data) — storage configuration and asynchronous hydration. - [Migrate persisted state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/migrate-persisted-state) — keep older shared links compatible when the persisted shape changes. # Test stores and components Test actions through the store API, then mount the connected React component and test what the user sees. Reset the stores after every test so one test's updates do not become another test's starting state. Use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) for components and [Mock Service Worker (MSW)](https://mswjs.io/) for network requests, without replacing your application's actions or `fetch`. The examples below render a counter starting at `1`. Clicking **One Up** changes it to `2`; clicking **Load greeting** displays the response text from your greeting endpoint. The tests supply their own greeting through MSW. ## 1. Add the testing tools Start in a TypeScript React project with [Zustand](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand) installed. Choose one runner. Both setups use JSDOM to mount React components in a DOM environment. ### Vitest ```bash npm install -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom msw ``` ### Jest ```bash npm install -D jest jest-environment-jsdom ts-jest ts-node @types/jest @testing-library/react @testing-library/user-event @testing-library/jest-dom msw ``` Jest and Vitest have different module-loading conventions. Use the configuration for your runner rather than mixing Jest's CommonJS mock loading with Vitest's ES module loading. For projects with additional transforms, follow the [Jest setup documentation](https://jestjs.io/docs/getting-started) or [Vitest setup documentation](https://vitest.dev/guide). ## 2. Create the store and connected component Keep [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and the [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator)-typed initializer in application code so the tests exercise the real counter and network actions; see [Subscribe outside React](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/subscribe-outside-react) for the store API used to inspect and reset state. ```ts title="src/store.ts" import { create, type StateCreator } from 'zustand' export type CounterState = { count: number greeting: string inc: () => void loadGreeting: () => Promise } const counterStoreCreator: StateCreator = (set) => ({ count: 1, greeting: '', inc: () => set((state) => ({ count: state.count + 1 })), loadGreeting: async () => { const response = await fetch('https://api.example.com/greeting') if (!response.ok) { throw new Error(`Greeting request failed: ${response.status}`) } const greeting = await response.text() set({ greeting }) }, }) export const useCounterStore = create()(counterStoreCreator) ``` Replace `https://api.example.com/greeting` with your endpoint in the application and the test handler. This example expects a text response. Select the values and actions the component needs. No provider is required for this bound store. ```tsx title="src/Counter.tsx" import * as React from 'react' import { useCounterStore } from './store' export function Counter() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) const greeting = useCounterStore((state) => state.greeting) const loadGreeting = useCounterStore((state) => state.loadGreeting) return (

Counter Store

{count}

{greeting}

) } ``` The tests below mount this component with `render()`. They check both the store's state and the rendered count after an update. You can also connect a component through [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore), passing the store API and a selector. This alternative counter uses the same store and renders the same count and increment button. ```tsx title="src/CounterWithUseStore.tsx" import * as React from 'react' import { useStore } from 'zustand' import { useCounterStore } from './store' export function CounterWithUseStore() { const count = useStore(useCounterStore, (state) => state.count) const inc = useStore(useCounterStore, (state) => state.inc) return (

Counter Store

{count}

) } ``` ![The counter displays its initial count of 1 and the One Up button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/a88adffb176ac321e2695a6e83bc6d8b.png) ## 3. Reset state and intercept requests Put the shared test support in one file. `getInitialState()` returns the state created by the initializer; `getState()` returns the current state. Restore the complete initial state with `setState(initialState, true)`, following the authors' reset pattern. Keep this reset in test teardown, not in application actions. ```ts title="tests/support.ts" import { act, cleanup } from '@testing-library/react' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { useCounterStore } from '../src/store' const initialState = useCounterStore.getInitialState() const storeResetFns = new Set<() => void>([ () => useCounterStore.setState(initialState, true), ]) export const server = setupServer( http.get('https://api.example.com/greeting', () => HttpResponse.text('Hello from the test'), ), ) export function resetAfterTest() { cleanup() act(() => { storeResetFns.forEach((reset) => reset()) }) server.resetHandlers() } ``` Add a reset function for each additional shared store your tests use. This example registers the real store explicitly: it does not mock the `zustand` module. MSW intercepts the request while the real `loadGreeting()` action still runs. ### Vitest setup ```ts title="tests/setup-vitest.ts" import '@testing-library/jest-dom/vitest' import { afterAll, afterEach, beforeAll } from 'vitest' import { resetAfterTest, server } from './support' beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterEach(resetAfterTest) afterAll(() => server.close()) ``` ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['./tests/setup-vitest.ts'], include: ['tests/**/*.vitest.test.tsx'], }, }) ``` ### Jest setup ```ts title="tests/setup-jest.ts" import '@testing-library/jest-dom/jest-globals' import { afterAll, afterEach, beforeAll } from '@jest/globals' import { resetAfterTest, server } from './support' beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterEach(resetAfterTest) afterAll(() => server.close()) ``` ```ts title="jest.config.ts" const config = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['./tests/setup-jest.ts'], testMatch: ['**/tests/**/*.jest.test.tsx'], transform: { '^.+\\.tsx?$': [ 'ts-jest', { tsconfig: { strict: true, jsx: 'react-jsx', module: 'CommonJS' } }, ], }, } export default config ``` Use a Node runtime and test environment compatible with your installed MSW version. Follow [MSW's Node integration instructions](https://mswjs.io/docs/integrations/node) for any required environment support. ## 4. Assert actions, rendered updates, and asynchronous results Use your runner's test file below. The first test calls the real action inside `act()` on a mounted component. The second uses the button, checking that the counter starts at `1` again. The third waits for the network action's result in the DOM rather than sleeping for a fixed duration. ### Vitest tests ```tsx title="tests/counter.vitest.test.tsx" import * as React from 'react' import { expect, test } from 'vitest' import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from '../src/Counter' import { useCounterStore } from '../src/store' test('the action updates the store and its connected component', () => { render() expect(useCounterStore.getState().count).toBe(1) act(() => useCounterStore.getState().inc()) expect(useCounterStore.getState().count).toBe(2) expect(screen.getByLabelText('Count')).toHaveTextContent('2') }) test('a button updates the counter from its initial state', async () => { const user = userEvent.setup() render() expect(screen.getByLabelText('Count')).toHaveTextContent('1') await user.click(screen.getByRole('button', { name: 'One Up' })) expect(screen.getByLabelText('Count')).toHaveTextContent('2') expect(useCounterStore.getState().count).toBe(2) }) test('the real network action displays the intercepted response', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: 'Load greeting' })) expect(await screen.findByText('Hello from the test')).toBeInTheDocument() expect(useCounterStore.getState().greeting).toBe('Hello from the test') }) ``` ```bash npx vitest run ``` ### Jest tests ```tsx title="tests/counter.jest.test.tsx" import * as React from 'react' import { expect, test } from '@jest/globals' import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from '../src/Counter' import { useCounterStore } from '../src/store' test('the action updates the store and its connected component', () => { render() expect(useCounterStore.getState().count).toBe(1) act(() => useCounterStore.getState().inc()) expect(useCounterStore.getState().count).toBe(2) expect(screen.getByLabelText('Count')).toHaveTextContent('2') }) test('a button updates the counter from its initial state', async () => { const user = userEvent.setup() render() expect(screen.getByLabelText('Count')).toHaveTextContent('1') await user.click(screen.getByRole('button', { name: 'One Up' })) expect(screen.getByLabelText('Count')).toHaveTextContent('2') expect(useCounterStore.getState().count).toBe(2) }) test('the real network action displays the intercepted response', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: 'Load greeting' })) expect(await screen.findByText('Hello from the test')).toBeInTheDocument() expect(useCounterStore.getState().greeting).toBe('Hello from the test') }) ``` ```bash npx jest ``` `Hello from the test` is the fixture supplied by the handler, not a promise about what your real service returns. In your application, the status paragraph displays your endpoint's response text. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `setState`'s `replace` argument | `boolean` | For object updates, omitted means merge | Pass `true` in teardown to restore the complete initial state rather than shallow-merge it with the current state. | ## Pitfalls - A module-level store survives component unmounts. Reset every shared store used by the suite; DOM cleanup alone does not reset its state. - Wrap direct store updates affecting mounted components in `act()`, including teardown resets. Await user interactions and asynchronous DOM assertions. - If you adopt automatic Zustand mocks, put Vitest's manual-mock directory under its configured `root`. The explicit-reset setup above needs no manual-mock directory. - For replacement-state typing and preservation of actions, see [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware). For selector stability, see [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions). ## Live demos Explore the authors' [Jest testing demo](https://stackblitz.com/edit/jest-zustand) or [Vitest testing demo](https://stackblitz.com/edit/vitest-zustand) for their automatic store-reset mock and counter component tests. ## Related - [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state) — reset one store or multiple stores. - [Initialize scoped stores](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/initialize-scoped-stores) — supply a scoped store through React context. - [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) — immutable updates and colocated actions. # Use reducers and dispatch Use the shipped [`redux`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#redux) middleware when you want typed action objects and a reducer to drive store updates. This example renders a person form: editing a field dispatches an action and updates the displayed name and email. Reducers are optional. Zustand's recommended Flux-inspired pattern colocates state and update functions without requiring dispatch; see [Update state with actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-state-with-actions) for that approach. ## 1. Create the reducer-backed store In your React project, install Zustand: ```bash npm install zustand ``` Define your application state and a discriminated union of actions. Each action has a string `type` and the payload its reducer branch needs. Pass the reducer and initial state to `redux`, then pass the resulting state creator to [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create). ```ts title="person-store.ts" import { create } from 'zustand' import { redux } from 'zustand/middleware' type PersonState = { firstName: string lastName: string email: string } type PersonAction = | { type: 'person/setFirstName'; firstName: string } | { type: 'person/setLastName'; lastName: string } | { type: 'person/setEmail'; email: string } function personReducer(state: PersonState, action: PersonAction): PersonState { switch (action.type) { case 'person/setFirstName': return { ...state, firstName: action.firstName } case 'person/setLastName': return { ...state, lastName: action.lastName } case 'person/setEmail': return { ...state, email: action.email } default: return state } } const initialState: PersonState = { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', } export const usePersonStore = create(redux(personReducer, initialState)) ``` The result is a store hook with the initial person data and a typed `dispatch` function. The middleware adds `dispatch` both to the state and to the store API; you do not need to implement it yourself. ## 2. Render the form and dispatch from its controls Use this browser entry point in a React project whose HTML contains `
`. Select each field and `dispatch` separately, following the starter's selector pattern. No provider is required. ```tsx title="main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { usePersonStore } from './person-store' function PersonForm() { const firstName = usePersonStore((state) => state.firstName) const lastName = usePersonStore((state) => state.lastName) const email = usePersonStore((state) => state.email) const dispatch = usePersonStore((state) => state.dispatch) return (

Edit person

{firstName} {lastName} ({email})

) } const container = document.getElementById('root')! createRoot(container).render( , ) ``` The form starts with Barbara Hepworth's name and email. Edit any input: `dispatch` passes the current state and action to the reducer, and the selected field and summary update. Each reducer branch copies the state and changes only its target field. `dispatch(action)` returns the action you passed, not the new state. Outside React, you can call `usePersonStore.dispatch(action)` on this same store; read the current state with `usePersonStore.getState()`. ## Parameters that matter `redux` takes two required arguments, not an options object: | Argument | Type | Default | What it does | | --- | --- | --- | --- | | `reducer` | `(state: T, action: A) => T` | Required | Computes the next state from the current state and action. `A` must extend `{ type: string }`. | | `initialState` | `T` | Required | Supplies the initial state alongside the middleware's `dispatch` function. | ## Pitfalls - Keep the reducer pure: return the next state rather than mutating the current state. Put side effects in functions that wrap dispatch, not in the reducer. - Reserve the `dispatch` field for the middleware. Its initialization spreads your initial state after adding `dispatch`, so an initial-state field with that name overrides the state-level function. - Middleware composition has typing and ordering constraints. See [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) when adding more middleware to this store. ## Live demo and related pages Try the [Zustand live demo](https://zustand-demo.pmnd.rs/) to see a rendered counter driven by a store hook. It demonstrates hook-based updates, rather than this reducer-backed person form. - [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start) — Set up a mounted React store. - [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions) — Choose which state each component observes. - [Debug store updates](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/debug-store-updates) — Inspect updates with Redux DevTools. # zustand 🐻 Bear necessities for state management in React ## Install ```bash npm install zustand ``` Install these too only if you use what needs them: `@types/react`, `immer`, `react`, `use-sync-external-store`. ## Functions ### `useStore` ```ts function useStore>( api: S, ): ExtractState function useStore, U>( api: S, selector: (state: ExtractState) => U, ): U ``` ## Constants ### `create` ```ts const create: Create ``` ### `createStore` ```ts const createStore: CreateStore ``` ## Interfaces ### `StoreApi` ```ts interface StoreApi ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `setState` | `SetStateInternal` | | | | `getState` | `() => T` | | | | `getInitialState` | `() => T` | | | | `subscribe` | `(listener: (state: T, prevState: T) => void) => () => void` | | | ### `StoreMutators` Also has every member of `StoreMutators`, listed on its own entry. ```ts interface StoreMutators ``` ## Types ### `ExtractState` ```ts type ExtractState = S extends { getState: () => infer T } ? T : never ``` ### `Mutate` ```ts type Mutate = number extends Ms['length' & keyof Ms] ? S : Ms extends [] ? S : Ms extends [[infer Mi, infer Ma], ...infer Mrs] ? Mutate[Mi & StoreMutatorIdentifier], Mrs> : never ``` ### `StateCreator` ```ts type StateCreator< T, Mis extends [StoreMutatorIdentifier, unknown][] = [], Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T, > = (( setState: Get, Mis>, 'setState', never>, getState: Get, Mis>, 'getState', never>, store: Mutate, Mis>, ) => U) & { $$storeMutators?: Mos } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `$$storeMutators?` | `Mos` | | | ### `StoreMutatorIdentifier` Also has every member of `String`, listed on its own entry. ```ts type StoreMutatorIdentifier = keyof StoreMutators ``` ### `UseBoundStore` Also has every member of `StoreApi`, listed on its own entry. ```ts type UseBoundStore> = { (): ExtractState (selector: (state: ExtractState) => U): U } & S ``` # zustand/middleware Part of the `zustand` package: install `zustand` and import these from `zustand/middleware`. ## Functions ### `combine` ```ts function combine< T extends object, U extends object, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( initialState: T, create: StateCreator, ): StateCreator, Mps, Mcs> ``` ### `createJSONStorage` ```ts function createJSONStorage( getStorage: () => StateStorage, options?: JsonStorageOptions, ): PersistStorage | undefined ``` ### `unstable_ssrSafe` ```ts function ssrSafe< T extends object, U extends object, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( config: StateCreator, isSSR: boolean = typeof window === 'undefined', ): StateCreator ``` ## Constants ### `devtools` ```ts const devtools: Devtools ``` ### `persist` ```ts const persist: Persist ``` ### `redux` ```ts const redux: Redux ``` ### `subscribeWithSelector` ```ts const subscribeWithSelector: SubscribeWithSelector ``` ## Interfaces ### `DevtoolsOptions` ```ts interface DevtoolsOptions extends Config ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name?` | `string` | | | | `enabled?` | `boolean` | | | | `anonymousActionType?` | `string` | | | | `store?` | `string` | | | ### `PersistOptions` ```ts interface PersistOptions< S, PersistedState = S, PersistReturn = unknown, > ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | `string` | | Name of the storage (must be unique) | | `storage?` | `PersistStorage \| undefined` | `createJSONStorage(() => window.localStorage)` | Use a custom persist storage. | | `partialize?` | `(state: S) => PersistedState` | | Filter the persisted value. | | `onRehydrateStorage?` | `( state: S, ) => ((state?: S, error?: unknown) => void) \| void` | | A function returning another (optional) function. The main function will be called before the state rehydration. The returned function will be called after the state rehydration or when an error occurred. | | `version?` | `number` | | If the stored state's version mismatch the one specified here, the storage will not be used. This is useful when adding a breaking change to your store. | | `migrate?` | `( persistedState: unknown, version: number, ) => PersistedState \| Promise` | | A function to perform persisted state migration. This function will be called when persisted state versions mismatch with the one specified here. | | `merge?` | `(persistedState: unknown, currentState: S) => S` | | A function to perform custom hydration merges when combining the stored state with the current one. By default, this function does a shallow merge. | | `skipHydration?` | `boolean` | `false` | An optional boolean that will prevent the persist middleware from triggering hydration on initialization, This allows you to call `rehydrate()` at a specific point in your apps rendering life-cycle. | ### `PersistStorage` ```ts interface PersistStorage ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `getItem` | `( name: string, ) => StorageValue \| null \| Promise \| null>` | | | | `setItem` | `(name: string, value: StorageValue) => R` | | | | `removeItem` | `(name: string) => R` | | | ### `StateStorage` ```ts interface StateStorage ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `getItem` | `(name: string) => string \| null \| Promise` | | | | `setItem` | `(name: string, value: string) => R` | | | | `removeItem` | `(name: string) => R` | | | ## Types ### `NamedSet` ```ts type NamedSet = WithDevtools>['setState'] ``` ### `StorageValue` ```ts type StorageValue = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `state` | `S` | | | | `version?` | `number` | | | # zustand/shallow Part of the `zustand` package: install `zustand` and import these from `zustand/shallow`. ## Functions ### `shallow` ```ts function shallow(valueA: T, valueB: T): boolean ``` ### `useShallow` ```ts function useShallow(selector: (state: S) => U): (state: S) => U ``` # zustand/react/shallow Part of the `zustand` package: install `zustand` and import these from `zustand/react/shallow`. ## Functions ### `useShallow` ```ts function useShallow(selector: (state: S) => U): (state: S) => U ``` # zustand/traditional Part of the `zustand` package: install `zustand` and import these from `zustand/traditional`. ## Functions ### `useStoreWithEqualityFn` ```ts function useStoreWithEqualityFn>( api: S, ): ExtractState function useStoreWithEqualityFn, U>( api: S, selector: (state: ExtractState) => U, equalityFn?: (a: U, b: U) => boolean, ): U ``` ## Constants ### `createWithEqualityFn` ```ts const createWithEqualityFn: CreateWithEqualityFn ``` ## Types ### `UseBoundStoreWithEqualityFn` Also has every member of `StoreApi`, listed on its own entry. ```ts type UseBoundStoreWithEqualityFn> = { (): ExtractState ( selector: (state: ExtractState) => U, equalityFn?: (a: U, b: U) => boolean, ): U } & S ``` # zustand/vanilla/shallow Part of the `zustand` package: install `zustand` and import these from `zustand/vanilla/shallow`. ## Functions ### `shallow` ```ts function shallow(valueA: T, valueB: T): boolean ``` # zustand/middleware/immer Part of the `zustand` package: install `zustand` and import these from `zustand/middleware/immer`. ## Constants ### `immer` ```ts const immer: Immer ``` # zustand/react Part of the `zustand` package: install `zustand` and import these from `zustand/react`. ## Also exported from here These names are documented with the package that defines them, and can be imported from this one too. - From [zustand](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand): `create`, `UseBoundStore`, `useStore` # zustand/vanilla Part of the `zustand` package: install `zustand` and import these from `zustand/vanilla`. ## Also exported from here These names are documented with the package that defines them, and can be imported from this one too. - From [zustand](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand): `createStore`, `ExtractState`, `Mutate`, `StateCreator`, `StoreApi`, `StoreMutatorIdentifier`, `StoreMutators`