# zustand · GPT-5.6 Luna # React quick start ## Prerequisites Use React 18 or later and TypeScript 4.5 or later. Zustand 5.0.15 requires React `>=18.0.0` and TypeScript 4.5 or later for its published types. The package requires Node.js `>=12.20.0`. ## Install Zustand In your React project, run: ```bash npm install zustand ``` ## Connect a typed store to React Use a store when several components need the same state or when you want the update logic beside the state it changes. This example creates a typed store, selects its state with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore), and mounts a component that toggles the displayed value. 1. Create the store. [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) gives you a store API that can be used outside React and passed to `useStore`. ```ts title="src/store.ts" import { createStore } from 'zustand/vanilla' type CounterStore = { enabled: boolean toggle: () => void } export const counterStore = createStore((set, get) => ({ enabled: false, toggle: () => set({ enabled: !get().enabled }), })) ``` 2. Select the state and action in a component. Each selector supplies one value from the store to the component. ```tsx title="src/App.tsx" import { useStore } from 'zustand' import { counterStore } from './store' export function App() { const enabled = useStore(counterStore, (state) => state.enabled) const toggle = useStore(counterStore, (state) => state.toggle) return (

Feature is {enabled ? 'enabled' : 'disabled'}

) } ``` 3. Mount the component in the browser. ```tsx title="src/main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { App } from './App' const rootElement = document.getElementById('root') if (rootElement === null) { throw new Error('Root element not found') } createRoot(rootElement).render( , ) ``` The mounted page initially shows `Feature is disabled`. Clicking **Toggle feature** changes it to `Feature is enabled`; clicking again changes it back. Keep the store outside the component so React re-renders from the same store instance instead of creating a new store on each render. ## Watch out for - Select individual values or actions when possible. A selector that creates a new reference on every render needs the approach described in [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). - Do not use a module-level global store for request-specific state in server-rendered applications. See [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores). ## Where to go next - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) explains the store, immutable updates, selectors, actions, and vanilla stores. - [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) develops the TypeScript patterns used by larger stores. - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) explains selection and render behavior, including stable outputs for computed selections. Try the authors' [live demo](https://zustand-demo.pmnd.rs/) to see a mounted Zustand application. # How Zustand works Zustand connects a store, the actions that update it, and selectors that let React components subscribe to only the state they use. Install Zustand and React in a React project: ```bash npm install zustand react ``` ```mermaid flowchart LR A["create()"] --> B["Store: state + actions"] B --> C["Selector"] C --> D["React component"] B --> E["setState()"] E --> B F["createStore()"] --> G["Vanilla StoreApi"] G --> H["useStore()"] H --> D ``` ## Updates are immutable and merge shallowly Use the `set` function supplied to the initializer, or the store's `setState`, for updates. An object update is shallowly merged into the current state; a function update receives the current state. Nested objects need their own spread so that the untouched nested properties remain present. ```tsx import { create } from 'zustand' type ProfileStore = { profile: { name: string settings: { compact: boolean } } rename: (name: string) => void setCompact: (compact: boolean) => void } const useProfile = create((set) => ({ profile: { name: 'Ada', settings: { compact: false }, }, rename: (name) => set((state) => ({ profile: { ...state.profile, name } })), setCompact: (compact) => set((state) => ({ profile: { ...state.profile, settings: { ...state.profile.settings, compact }, }, })), })) export function Profile() { const profile = useProfile((state) => state.profile) const rename = useProfile((state) => state.rename) return ( ) } ``` Typing in the input replaces only `profile.name`; the nested `settings` object remains part of the state because the update merges each level explicitly. Passing the replace flag to `setState` instead replaces the complete state model, including actions, so use it only when that is the intended result. The underlying store applies the merge or replacement and notifies listeners only when the next state is not `Object.is`-equal to the current state. ## Selectors and equality control rendering A selector narrows what a component reads. The React binding compares the selected result with `Object.is`, which makes atomic selections such as a number or function efficient. A selector that constructs a new object or array produces a new reference; wrap that selector with [`useShallow`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-react-shallow#useshallow) when shallow-equal results should reuse the previous reference. ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type BasketStore = { apples: number oranges: number addApple: () => void } const useBasket = create((set) => ({ apples: 0, oranges: 0, addApple: () => set((state) => ({ apples: state.apples + 1 })), })) export function BasketSummary() { const fruit = useBasket( useShallow((state) => ({ apples: state.apples, oranges: state.oranges })), ) return

{fruit.apples} apples, {fruit.oranges} oranges

} ``` The component renders the two counts as one selected object. `useShallow` returns the previous object when its selected properties are shallow-equal. For a single primitive, select it directly instead of constructing an object. ## Actions stay with the state Zustand recommends a single store for an application's global state, with actions defined directly on that store. An action can be asynchronous: wait for the work, then call `set`. Use `get` in the initializer when an action needs the current state outside a functional update. Reducer-style actions remain an optional pattern rather than the store's required layer. The initializer receives a typed [`StateCreator`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#statecreator), whose arguments are the update function, the getter, and the store API. The store returned by [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) is a [`UseBoundStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#useboundstore): it is callable as a hook and also exposes the store API. [`ExtractState`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#extractstate) obtains the state type from an API; [`StoreApi`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storeapi) describes `setState`, `getState`, `getInitialState`, and `subscribe`. The generic type plumbing for middleware uses [`Mutate`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#mutate), [`StoreMutatorIdentifier`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storemutatoridentifier), and [`StoreMutators`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storemutators). These types describe how a store API is changed by mutators; use them when extending middleware typings, not as a replacement for the store hook. ## A vanilla store separates creation from React Use [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) from the [`zustand/vanilla`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-vanilla) package when the store must exist without React. It returns a vanilla API rather than a hook. The API reads current state, updates it, exposes the initial state, and lets non-React code subscribe. ```tsx import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' type ClockStore = { label: string setLabel: (label: string) => void } const clockStore = createStore((set) => ({ label: 'Ready', setLabel: (label) => set({ label }), })) export function Clock() { const label = useStore(clockStore, (state) => state.label) const setLabel = useStore(clockStore, (state) => state.setLabel) return } ``` The button initially shows `Ready`; clicking it updates the vanilla store and the bound component shows `Started`. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) subscribes the component to that store without turning the store itself into a React hook. This separation also supports scoped or per-request stores when module-global state is unsafe in server-rendered applications. Do not assume middleware that changes the initializer's `set` or `get` also changes a vanilla store's direct `getState` and `setState`; that boundary is covered in [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store). ## Where to go next - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) for a complete application store. - [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates) for nested update patterns. - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) for selector stability and equality. - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) for non-React and scoped stores. - [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores) for per-request state and server-rendered applications. # Immutable state updates Zustand keeps state immutable: update through the store API, and return new references for the parts that changed. The default update is a shallow merge; nested values and complete-state replacement are explicit choices. ## How the update works The [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) function gives a React component a bound store hook. Its initializer receives `set`, which accepts a partial object or an updater function. Zustand compares the updater result with the current state using `Object.is`; when the result differs, it either shallowly merges the object or replaces the state. ```mermaid flowchart LR A["set(updater)"] --> B["next state"] B --> C{"Object.is(next, current)?"} C -->|yes| D["no notification"] C -->|no| E{"replace = true?"} E -->|no| F["shallow merge one level"] E -->|yes| G["replace complete state"] F --> H["notify subscribers"] G --> H ``` The reference check is about the state value returned by the updater. For an object nested inside that state, mutate-in-place can leave the nested reference unchanged, so return a new nested object instead. ## Shallow updates Use a partial object for a flat update. The other top-level properties remain in the store: ```tsx import { create } from 'zustand' type PersonState = { firstName: string lastName: string updateFirstName: (firstName: string) => void } const usePersonStore = create()((set) => ({ firstName: 'Ada', lastName: 'Lovelace', updateFirstName: (firstName) => set({ firstName }), })) export function PersonEditor() { const firstName = usePersonStore((state) => state.firstName) const lastName = usePersonStore((state) => state.lastName) const updateFirstName = usePersonStore((state) => state.updateFirstName) return (

{firstName} {lastName}

) } ``` Typing in the input updates `firstName` while `lastName` stays in the state. The component selects each value separately, so each selected primitive changes only when that value changes. ## Nested updates The merge stops at the first level. Preserve each object on the path to the field you change: ```tsx import { create } from 'zustand' type Settings = { theme: string density: 'comfortable' | 'compact' } type SettingsState = { settings: Settings setTheme: (theme: string) => void } const useSettingsStore = create()((set) => ({ settings: { theme: 'light', density: 'comfortable' }, setTheme: (theme) => set((state) => ({ settings: { ...state.settings, theme }, })), })) export function SettingsPanel() { const settings = useSettingsStore((state) => state.settings) const setTheme = useSettingsStore((state) => state.setTheme) return (

Theme: {settings.theme}

Density: {settings.density}

) } ``` Clicking the button changes `theme` and keeps `density`. Omitting `...state.settings` would create a new `settings` object without `density`, because the default merge does not recursively merge nested objects. For deeper structures, copy every changed level, or use the [Immer update guide](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer). ## Replacing the complete state Pass `true` as the second argument to `set` when the returned value is the complete state model. Replacement also removes properties that are not in the new value, including actions in a store that defines actions in its state. This standalone example uses [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) from the [`zustand/vanilla`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-vanilla) package and binds it to React with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore): ```tsx import { createStore } from 'zustand/vanilla' import { useStore } from 'zustand' type NoticeState = { message: string visible: boolean } const noticeStore = createStore(() => ({ message: 'Saved', visible: true, })) export function Notice() { const notice = useStore(noticeStore) return (

{notice.visible ? notice.message : 'No notice'}

) } ``` Before the click, the component shows `Saved`. After the click, replacement changes the complete `NoticeState` value and the component shows `No notice`. Use replacement only when you have every property required by the state type. ## Collections and references `Map` and `Set` are mutable objects, so create a new instance when changing one. The new instance gives the selected value a new reference: ```ts import { createStore } from 'zustand/vanilla' type TagState = { tags: Set } const tagStore = createStore(() => ({ tags: new Set() })) const addTag = (tag: string) => { tagStore.setState((state) => ({ tags: new Set(state.tags).add(tag), })) } ``` Do not mutate `state.tags` and return the same `Set`. Its reference remains equal, so a selector of `tags` does not observe a changed reference. Type empty collections explicitly when TypeScript cannot infer their element types. ## Next steps - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) explains how selected references affect re-renders. - [Update nested state with Immer](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer) covers the middleware and its pitfalls. - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) develops the standalone-store pattern. # Selectors and rendering Selectors decide which part of a store a component reads; the selected result and its equality function decide whether that component renders again. ## How the pieces fit ```mermaid flowchart LR Store["Zustand store"] --> Selector["selector(state)"] Selector --> Result["selected result"] Result --> Equality["Object.is or equality function"] Equality -->|different| Render["component renders"] Equality -->|equal| Skip["component keeps its render"] ``` The default comparison is `Object.is`. Selecting an atomic value such as a number, string, or action usually gives a stable result. A selector that constructs an object or array creates a new reference, even when its contents are unchanged, so use a stable selector output or a shallow comparison for that case. ## Select atomic values directly Create the store with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create), then pass a selector to the returned hook. Each selector subscribes the component to one result, so changing another state property does not change that result. ```tsx import { create } from 'zustand' type MealStore = { papaBear: string count: number setMeal: (meal: string) => void } const useMealStore = create()((set) => ({ papaBear: 'large porridge-pot', count: 0, setMeal: (meal) => set({ papaBear: meal }), })) export default function App() { const meal = useMealStore((state) => state.papaBear) const setMeal = useMealStore((state) => state.setMeal) return (

Meal: {meal}

) } ``` ![The mounted counter displays the selected meal and an Order pizza button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/fcb7f26b7bc4ad62596107075994f82d.png) The component reads the meal and action separately. Clicking the button changes the displayed meal; the `count` state is not part of either selected result. ## Select several values with `useShallow` Wrap a computed selector with [`useShallow`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-react-shallow#useshallow) when it returns an object or array whose entries are shallow-equal to the previous result. ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type FruitStore = { apples: number oranges: number addApple: () => void } const useFruitStore = create()((set) => ({ apples: 1, oranges: 2, addApple: () => set((state) => ({ apples: state.apples + 1 })), })) export default function App() { const fruit = useFruitStore( useShallow((state) => ({ apples: state.apples, oranges: state.oranges })), ) const addApple = useFruitStore((state) => state.addApple) return (

Apples: {fruit.apples}; oranges: {fruit.oranges}

) } ``` ![The mounted fruit component displays apple and orange counts with an Add apple button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/3440560938e07531713306376e40f618.png) For a direct comparison, [`shallow`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-shallow#shallow) takes two values and returns a boolean. It compares object entries and ordered iterable values using `Object.is` for their entries; use it when you need to supply an equality function yourself. ## Use a custom equality function In v5, [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) does not accept a custom equality function. Use [`createWithEqualityFn`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-traditional#createwithequalityfn) from `zustand/traditional` when a store needs a default equality function. The `zustand/traditional` entry point requires the `use-sync-external-store` package. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type PositionStore = { x: number y: number move: (x: number, y: number) => void } const usePositionStore = createWithEqualityFn()( (set) => ({ x: 0, y: 0, move: (x, y) => set({ x, y }), }), shallow, ) export default function App() { const position = usePositionStore((state) => ({ x: state.x, y: state.y })) const move = usePositionStore((state) => state.move) return (

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

) } ``` ![The mounted position component displays coordinates and a Move button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/4548a2bd8230a08dc322feb961849b13.png) The position object is compared shallowly, so the component re-renders when `x` or `y` changes and does not re-render for an update that leaves both selected values equal. Install the peer package before using this entry point: ```bash npm install zustand react use-sync-external-store ``` When the store is vanilla and the equality choice belongs to a particular component, use [`useStoreWithEqualityFn`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-traditional#usestorewithequalityfn) with the store API, a selector, and that component's equality function. ```tsx import { createStore } from 'zustand' import { shallow } from 'zustand/shallow' import { useStoreWithEqualityFn } from 'zustand/traditional' type PositionStore = { x: number y: number move: (x: number, y: number) => void } const positionStore = createStore()((set) => ({ x: 0, y: 0, move: (x, y) => set({ x, y }), })) export default function App() { const position = useStoreWithEqualityFn( positionStore, (state) => ({ x: state.x, y: state.y }), shallow, ) const move = useStoreWithEqualityFn(positionStore, (state) => state.move, shallow) return (

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

) } ``` ![The mounted vanilla-store component displays coordinates and a Move button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/4548a2bd8230a08dc322feb961849b13.png) Install React explicitly when using either React entry point; `npm install zustand` does not add this optional peer dependency. ```bash npm install zustand react ``` ## Bind a vanilla store when the store itself is separate For a store created outside React, [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) returns a store API. Pass that API to [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) and give the hook a selector. This keeps the same selector-and-equality rule while allowing the store instance to be created and supplied by another part of the application. ```tsx import { createStore } from 'zustand' import { useStore } from 'zustand' type CounterStore = { count: number increment: () => void } const counterStore = createStore()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) export default function App() { const count = useStore(counterStore, (state) => state.count) const increment = useStore(counterStore, (state) => state.increment) return (

Count: {count}

) } ``` The component displays the counter from the separate store and updates when `count` changes. ## Avoid unstable selector outputs Do not return a fresh object or array from a `create` selector without handling its equality. In v5, the default `Object.is` comparison sees each fresh reference as different and the selector can cause an infinite update loop. Choose one of these forms instead: - Select each value separately. - Use `createWithEqualityFn` with an equality function when you need that store-wide behavior. Use a stable fallback value when a selector returns a fallback function; creating a new fallback function in the selector has the same unstable-reference problem. ## Related - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) - [Optimize computed selections](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/optimize-computed-selections) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) - [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) # Vanilla and scoped stores For application-wide state, use the hook form returned by [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create). Use [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) when creation must be separate from React, or when each scope needs its own store instance. Install the [`zustand`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand) package with `npm install zustand`. Its `zustand/vanilla` entry provides the standalone store creator, while its React entry provides the binding hook. ## How the parts fit together ```mermaid flowchart LR F["store factory"] --> V["vanilla store API"] V --> H["React store binding"] H --> C["React component"] P["React context"] --> H R["request boundary"] --> F ``` `createStore` returns a standalone store API. Its `getState`, `setState`, `subscribe`, and `getInitialState` methods let non-React code use the store. Pass that store and a selector to [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) in a component; the component then subscribes to the selected value. Use a module-level store when the state is genuinely global. Use a factory when initial values come from component props, when two parts of the tree must not share state, or when a server must create one store per request. Context carries the selected store instance to the component subtree; it does not replace the store API. For the direct bound-hook pattern, including atomic selections and the rendered result, see [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). ## Scope a store through context Use a factory and context when each provider instance needs independent state or receives initial props. Keep the store in `useState` so a provider re-render does not create a new store. When this scoped binding needs a custom equality function, install the `zustand/traditional` peer dependency with `npm install zustand react use-sync-external-store` and use [`useStoreWithEqualityFn`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-traditional#usestorewithequalityfn) in the context hook. ```tsx import { createContext, useContext, useState, type PropsWithChildren, type ReactNode, } from 'react' import { createStore } from 'zustand/vanilla' import { useStoreWithEqualityFn } from 'zustand/traditional' type CounterProps = { count: number } type CounterState = CounterProps & { increment: () => void } const createCounterStore = (props: Partial = {}) => createStore()((set) => ({ count: props.count ?? 0, increment: () => set((state) => ({ count: state.count + 1 })), })) type CounterStore = ReturnType const CounterContext = createContext(null) type CounterProviderProps = PropsWithChildren export function CounterProvider({ children, ...props }: CounterProviderProps): ReactNode { const [store] = useState(() => createCounterStore(props)) return ( {children} ) } function useCounterStore(selector: (state: CounterState) => T): T { const store = useContext(CounterContext) if (!store) { throw new Error('CounterProvider is missing') } return useStoreWithEqualityFn(store, selector, (left, right) => left === right) } export function Counter(): ReactNode { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) return ( ) } export function App(): ReactNode { return ( <> ) } ``` The two buttons start at `Count: 2` and `Count: 10`. Clicking one changes only its provider's store. The context wrapper also gives you a single place to reject consumers rendered outside the provider. ![Two mounted counters show Count: 2 and Count: 10.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/424bfa87a15fe5d592cc96db23e9ea42.png) ## Create one store per request A server can handle concurrent requests, so do not share request-specific state in a module-level store. Export a factory and call it at the request or provider boundary. On the client, initialize the provider's store once with `useState`; on the server and client, use matching initial data to avoid hydration differences. ```ts import { createStore } from 'zustand/vanilla' type RequestState = { requestId: string } export const createRequestStore = (requestId: string) => createStore()(() => ({ requestId })) export function loadRequestState(requestId: string): string { const store = createRequestStore(requestId) return store.getState().requestId } ``` Each call to `createRequestStore` returns a separate instance, so one request's state is not reused by another. React Server Components must not read from or write to the store; keep store access in client components and pass the request's initial data into the client-side provider. ## Choosing the boundary | Need | Store shape | Binding | | --- | --- | --- | | State shared by the application | One module-level store | `create` and its returned hook | | State used by React and non-React code | One standalone store | `createStore`, then `useStore` | | Independent instances in different subtrees | Factory-created stores | `createStore`, context, and `useStore` | | Request-specific or SSR state | One factory call per request | Create the store at the request/provider boundary | The lower-level store API is useful here because scoping is about which store instance a component receives. Do not add context merely to share a genuinely global store; use the direct hook for that case. See [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) for store construction details and [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores) for the server-rendering constraints. # Typed store patterns This page adds the type patterns for reusing a creator, extracting a store's complete shape, and carrying custom middleware changes through composition. For the roles of [`StateCreator`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#statecreator), [`StoreApi`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storeapi), [`UseBoundStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#useboundstore), [`ExtractState`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#extractstate), [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create), and [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore), see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works). ## The model A typed store has three related layers: ```mermaid flowchart LR T["State and actions: T"] --> C["StateCreator"] C --> R["create() or createStore()"] R --> A["UseBoundStore or StoreApi"] A --> X["ExtractState"] C --> M["Middleware mutator tuples"] M --> R ``` The addition here is the type flow: one creator can feed either binding, and `ExtractState` can recover the complete state-and-actions shape after the store is created. For the initializer arguments, bindings, and vanilla API, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works). The type parameter is important because the state type is used both as an input to `get` and as the initializer's result. TypeScript cannot reliably infer that invariant type from the initializer alone, so use the curried form when you provide the state type: `create()((set) => ...)` or `createStore()((set) => ...)`. ## Define the creator once Give the state and actions a single type, then export a creator that can be used by either a React-bound or vanilla store: ```ts title="counter-store.ts" import { type StateCreator } from 'zustand' export type CounterStore = { count: number increment: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }) ``` The creator returns both data and actions. `set` performs a shallow merge by default, so the action returns only the changed field. Treat nested objects and arrays as immutable: create a new nested value before passing it to `set`. ## Bind the creator in React Pass the creator to the curried form of [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create). The result is a callable store whose selector is typed as `CounterStore`: ```tsx title="Counter.tsx" import { create } from 'zustand' import { type CounterStore, counterStoreCreator } from './counter-store' const useCounterStore = create()(counterStoreCreator) export function Counter() { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) return ( ) } ``` The button reads one atomic value and one action. Clicking it updates the store and the component re-renders because its selected `count` changes. When a component needs several values, select them with a stable result or use `useShallow` from `zustand/react/shallow`; avoid subscribing to the whole store when only part of it is needed. ## Infer the store state Use [`ExtractState`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#extractstate) when a utility, test, or component prop needs the complete state-and-actions shape of an existing store. It reads the return type of `getState()` rather than making you repeat the type: ```ts title="counter-state.ts" import { create, type ExtractState } from 'zustand' type CounterStateShape = { count: number increment: () => void } const useCounterStore = create()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) export type CounterState = ExtractState export function formatCounter(state: CounterState): string { return `Count: ${state.count}` } export const currentCounter = formatCounter(useCounterStore.getState()) ``` `CounterState` includes `count` and `increment`. The store itself remains a `UseBoundStore`, so `getState()` returns that same inferred shape. ## Use a vanilla store with a React binding Use [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) when the store must exist independently of React—for example, when you create a scoped store for a particular owner. Bind that store in a component with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore): ```tsx title="ScopedCounter.tsx" import { useState } from 'react' import { createStore, type StoreApi } from 'zustand' import { useStore } from 'zustand' import { type CounterStore, counterStoreCreator } from './counter-store' function createCounterStore(): StoreApi { return createStore()(counterStoreCreator) } export function ScopedCounter() { const [counterStore] = useState(createCounterStore) const count = useStore(counterStore, (state) => state.count) const increment = useStore(counterStore, (state) => state.increment) return ( ) } ``` The store is created once for this component instance. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) subscribes to the vanilla API and returns the selected value, while the `StoreApi` annotation keeps direct store access typed outside the component. ## Compose middleware without losing the state type For middleware that wraps a creator, compose the shipped middleware directly inside `create`. This keeps the initializer contextually typed: ```ts title="persisted-counter.ts" import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' interface CounterState { count: number increment: () => void } export const usePersistedCounter = create()( devtools( persist( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'counter-store' }, ), ), ) ``` The store keeps `CounterState` while `persist` receives its storage name and `devtools` wraps the resulting creator. Keep this nesting immediately inside `create`; moving it into an untyped helper loses the contextual inference that supplies the creator's `set` type. For the general roles of [`Mutate`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#mutate), [`StateCreator`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#statecreator), [`StoreMutators`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storemutators), and [`StoreMutatorIdentifier`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storemutatoridentifier), see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works). This page adds the custom-middleware pattern: use `Mis` for mutators already present on the creator's input and `Mos` for mutators it produces, then make the declaration merge and runtime implementation agree: ```ts import type { Mutate, StateCreator, StoreApi, StoreMutatorIdentifier, StoreMutators, } from 'zustand' declare module 'zustand/vanilla' { interface StoreMutators { counterLabel: S & { counterLabel: A } } } type CounterLabel = < T, A, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mos extends [StoreMutatorIdentifier, unknown][] = [], >( creator: StateCreator, label: A, ) => StateCreator type LabeledCounterApi = Mutate< StoreApi, [['counterLabel', string]] > type CounterStore = { count: number } ``` The declaration describes the type-level contract; a working middleware also has to implement the corresponding runtime change. Do not use this extension pattern when ordinary shipped middleware already provides the behavior. ## Choose the pattern - Use `create()` when a React component can consume one global bound store. - Use `ExtractState` when another type needs the store's complete inferred shape. - Use `createStore()` and `useStore(store, selector)` when store creation must be separate from React or scoped to an owner. - Nest shipped middleware directly inside `create` so contextual typing reaches every creator. - Use mutator tuples and declaration merging only when you are implementing middleware that changes the store API. For immutable update details, see [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates). For vanilla-store lifecycles, see [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores). For middleware ordering and composition, see [Compose store middleware](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/compose-store-middleware). # Build a React store Use this page when a React component needs shared state, colocated actions, and a visible update. You create a hook with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create), select the state or action you need, and render the selected value. ## Prerequisites Use React 18 or later, TypeScript in strict mode, and Zustand 5.0.15. Install Zustand in your React project: ```bash npm install zustand ``` ## Build and mount the store Define state and its action together, then read the same store from separate mounted components. The page renders the current count and an `Increment` button; clicking the button updates every component that selects the count. ```tsx title="src/App.tsx" import { createRoot } from 'react-dom/client' import { create } from 'zustand' type CounterStore = { count: number increment: () => void } const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) function CounterValue() { const count = useCounterStore((state) => state.count) return

Count: {count}

} function CounterControls() { const increment = useCounterStore((state) => state.increment) return ( ) } function Counter() { return (
) } const rootElement = document.getElementById('root') if (!rootElement) { throw new Error('The root element is missing') } createRoot(rootElement).render() ``` ![The mounted counter displays Count: 0 and an Increment button in separate components.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/fb5ff6993ef4399a4529d5e9f8322035.png) The initial render shows `Count: 0` and the `Increment` button. The two components share the hook, so each click calls the colocated action and updates the value component. See the [live demo](https://zustand-demo.pmnd.rs/) or [its React example source](https://github.com/pmndrs/zustand/blob/d7a5583cffd80af515f7dfb69583c95cbdc9e2ce/examples/demo) for a complete application using the same store shape. ## Choose selectors for the values you use For selector granularity, equality, and stable computed results, see [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). Keep updates immutable. Zustand shallow-merges the object returned by `set` by default, but nested objects need an explicit nested merge. Passing the replace flag replaces the complete state model, including actions, so use that form only when replacing the whole store is intentional. See [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates). ## Where to go next - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) — control re-renders for computed selections. - [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) — organize larger TypeScript stores. - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) — create a store without a React hook. - [Split a store into slices](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/split-a-store-into-slices) — divide a growing store into smaller creators. # Persist store data Use [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) when a store must retain its state across page reloads. Give the store a unique `name`, choose a storage adapter, and account for the time at which hydration completes. ## Create a persisted React store Wrap the state creator passed to [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with `persist`. This example stores the bear count in `sessionStorage`; the mounted component displays the count and updates it when you click the button. ```tsx title="bear-store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' type BearStore = { bears: number addBear: () => void } export const useBearStore = create()( persist( (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), }), { name: 'bear-counter', storage: createJSONStorage(() => sessionStorage), }, ), ) ``` ```tsx title="BearCounter.tsx" import { useBearStore } from './bear-store' export function BearCounter() { const bears = useBearStore((state) => state.bears) const addBear = useBearStore((state) => state.addBear) return ( ) } ``` `name` is the storage key and is the only required persistence option, so use a different value for each store. If you omit `storage`, persistence uses JSON storage backed by `localStorage`. [`createJSONStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#createjsonstorage) converts a storage engine with `getItem`, `setItem`, and `removeItem` methods into the adapter expected by `persist`. ## Supply a custom storage engine Implement [`StateStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#statestorage) when the storage engine needs its own key handling or API. The adapter below prefixes every key while retaining the browser's synchronous storage behaviour. The storage getter is a function, so the adapter can obtain a browser storage object only when the store is created. ```tsx title="prefixed-bear-store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import type { StateStorage } from 'zustand/middleware' type BearStore = { bears: number addBear: () => void } const prefixedStorage: StateStorage = { getItem: (name) => sessionStorage.getItem(`demo:${name}`), setItem: (name, value) => sessionStorage.setItem(`demo:${name}`, value), removeItem: (name) => sessionStorage.removeItem(`demo:${name}`), } export const useBearStore = create()( persist( (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), }), { name: 'bear-counter', storage: createJSONStorage(() => prefixedStorage), }, ), ) ``` The adapter receives JSON strings from `createJSONStorage`; do not parse or stringify them again in these methods. The JSON helper uses `JSON.parse` and `JSON.stringify` without runtime shape validation, so validate untrusted or stale data in a custom adapter before treating it as store state. ## Handle asynchronous hydration `localStorage` and `sessionStorage` are synchronous. An asynchronous adapter returns a promise from `getItem`, so the first render can show the store's defaults while persisted data is loading. Wait for hydration before presenting UI that depends on the persisted value. ```tsx title="async-bear-store.ts" import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import type { StateStorage } from 'zustand/middleware' type BearStore = { bears: number addBear: () => void } const asyncStorage: StateStorage> = { getItem: async (name) => { await new Promise((resolve) => window.setTimeout(resolve, 100)) return sessionStorage.getItem(name) }, setItem: async (name, value) => { sessionStorage.setItem(name, value) }, removeItem: async (name) => { sessionStorage.removeItem(name) }, } export const useAsyncBearStore = create()( persist( (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), }), { name: 'async-bear-counter', storage: createJSONStorage(() => asyncStorage), }, ), ) ``` Register an `onFinishHydration` listener in the component and remove it when the component unmounts. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) reads the store's state; the listener changes the component's local `hydrated` flag when the persisted state has been merged. ```tsx title="AsyncBearCounter.tsx" import { useEffect, useState } from 'react' import { useStore } from 'zustand' import { useAsyncBearStore } from './async-bear-store' export function AsyncBearCounter() { const bears = useStore(useAsyncBearStore, (state) => state.bears) const addBear = useStore(useAsyncBearStore, (state) => state.addBear) const [hydrated, setHydrated] = useState(() => useAsyncBearStore.persist.hasHydrated(), ) useEffect(() => { const unsubscribe = useAsyncBearStore.persist.onFinishHydration(() => { setHydrated(true) }) return unsubscribe }, []) if (!hydrated) { return

Loading saved bears…

} return ( ) } ``` ![The mounted counter displays the persisted bear count after hydration.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/cb87993b983153eadb65eff5d2ac060c.png) The loading message can appear on the initial render with asynchronous storage. After hydration finishes, the component renders the persisted count. The `onFinishHydration` method returns the unsubscribe function, so the listener does not outlive the component. ## Options that matter here | Option | Type | Default | What it does | |---|---|---|---| | `name` | `string` | — | Selects the unique key used in storage. | | `storage` | storage adapter | `createJSONStorage(() => localStorage)` | Reads and writes the persisted value through a storage adapter. | | `onRehydrateStorage` | function or function returning a function | — | Runs custom logic before and after hydration; the returned callback receives the state or an error. | | `skipHydration` | `boolean \| undefined` | `undefined` | Prevents automatic initial hydration, leaving the first call to `rehydrate()` to the application. | Use `skipHydration` when the application controls when hydration begins, such as an SSR setup. Otherwise, let `persist` hydrate on initialization and gate asynchronous-storage UI as shown above. ## Related - [Persist selected state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-selected-state) filters which fields are stored. - [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) handles nested objects that need more than the default shallow merge. - [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration) covers manual hydration control. - [Migrate persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/migrate-persisted-state) handles versioned stored data. - See the [persist middleware reference](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) for the complete API. # Persist selected state Use `partialize` when a persisted store contains both user state and runtime-only state. The function returns the slice that [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) writes; the rest of the store remains available during the current session but is rebuilt from its initial value after a reload. ## Choose the persisted fields This example keeps the selected item across reloads but does not store the draft text. It uses [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with a React hook and the default JSON storage backed by `localStorage`. ```tsx title="main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { persist } from 'zustand/middleware' type Item = { id: string label: string } type AppStore = { selectedId: string | null draft: string select: (id: string) => void setDraft: (draft: string) => void } type PersistedAppStore = Pick const items: Item[] = [ { id: 'alpha', label: 'Alpha' }, { id: 'beta', label: 'Beta' }, ] const useAppStore = create()( persist( (set) => ({ selectedId: null, draft: '', select: (selectedId) => set({ selectedId }), setDraft: (draft) => set({ draft }), }), { name: 'selected-item-demo', partialize: (state): PersistedAppStore => ({ selectedId: state.selectedId, }), }, ), ) function App() { const selectedId = useAppStore((state) => state.selectedId) const draft = useAppStore((state) => state.draft) const select = useAppStore((state) => state.select) const setDraft = useAppStore((state) => state.setDraft) return (

Items

Selected: {selectedId ?? 'none'}

    {items.map((item) => (
  • ))}
) } const root = document.getElementById('root')! createRoot(root).render( , ) ``` ![The mounted example shows the item list, the current selection, and the draft note field.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/4a5b829213f9c14b58e02a5c49ba6689.png) Clicking a selection updates the rendered `Selected` value and writes only `selectedId` under the `selected-item-demo` storage key. Type text into `Draft note`, then reload: the selected item remains and the draft returns to its initial empty value. The persisted value contains the state returned by `partialize`, not the store's actions or the omitted `draft` field. ## `partialize` options that matter | Option | Type | Default | What it does | |---|---|---|---| | `name` | `string` | — | Chooses the unique storage key. | | `partialize` | `(state: State) => PersistedState` | `(state) => state` | Selects the value written to storage. | | `storage` | `PersistStorage` | `createJSONStorage(() => localStorage)` | Supplies the storage adapter used for reads and writes. | Return an object containing the fields you intend to restore. If the persisted selection is part of a nested object, read [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) before partially persisting it: the default merge is shallow and can replace an entire nested object. If the adapter is asynchronous, the first render can show defaults while hydration completes; see [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data). ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) covers storage adapters and hydration. - [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) covers custom merges for nested state. - See the [`persist` middleware reference](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) for the complete option set. # Merge partial persisted state Use a custom `merge` function when persisted data contains only part of a nested object and hydration must keep the fields already present in the current store. Build the store with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) and wrap its state creator with [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist). ## When to use it `persist` hydrates the store by merging the stored value with the current state. Its default merge is shallow: if storage contains `settings: { theme: 'dark' }`, it replaces the complete current `settings` object, including fields such as `density`. Supply a nested merge when the persisted object is partial. ## Example This complete React example hydrates a partial `settings` object and keeps the current `density` field. ```tsx import { create } from 'zustand' import { persist } from 'zustand/middleware' import { createRoot } from 'react-dom/client' type Settings = { theme: 'light' | 'dark' density: 'compact' | 'comfortable' } type Store = { settings: Settings } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } function getTheme(value: unknown): Settings['theme'] | undefined { if (!isRecord(value) || !isRecord(value.settings)) return undefined return value.settings.theme === 'light' || value.settings.theme === 'dark' ? value.settings.theme : undefined } localStorage.setItem( 'settings', JSON.stringify({ state: { settings: { theme: 'dark' } }, version: 0 }), ) const useStore = create()( persist( () => ({ settings: { theme: 'light', density: 'comfortable' } }), { name: 'settings', merge: (persistedState, currentState) => { const theme = getTheme(persistedState) return { ...currentState, settings: { ...currentState.settings, ...(theme === undefined ? {} : { theme }), }, } }, }, ), ) function SettingsView() { const settings = useStore((state) => state.settings) return

{settings.theme} / {settings.density}

} const root = document.getElementById('root') if (root !== null) createRoot(root).render() ``` After hydration, the mounted component displays `dark / comfortable`: the persisted theme applies and the current density remains. ## Options that matter here | Option | Type | Default | What it does | |---|---|---|---| | `name` | `string` | — | Names the storage entry. Use a unique name for the store. | | `partialize` | `(state: State) => Object` | `(state) => state` | Selects the fields written to storage. | | `merge` | `(persistedState: unknown, currentState: State) => State` | Shallow merge | Combines stored data with current state during hydration. | ## Pitfalls - A custom nested merge is needed when partial persistence targets nested objects. The default shallow merge can erase fields that are present only in the current nested object. - Async persisted storage hydrates after the initial render, so the UI can show default state temporarily. Wait for hydration when that timing matters; see [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration). ## Live demo Explore the [Zustand demo](https://zustand-demo.pmnd.rs/) for a mounted store application. ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) - [Persist selected state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-selected-state) - [Migrate persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/migrate-persisted-state) # Control persisted hydration Use `skipHydration` when the application must choose when persisted data enters the store, and use `onRehydrateStorage` to observe completion or report an error. ## When to use this By default, `persist` hydrates the store during initialization. In an SSR application, or whenever the first render must use only the initial state, set `skipHydration: true` and start hydration after the appropriate client-side lifecycle point. ## Start hydration after the component mounts Create the store with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) and wrap its initializer with [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist). The callback returned by `onRehydrateStorage` runs after hydration: its second argument is defined when hydration fails. ```tsx import { useEffect, useState } from 'react' import { create } from 'zustand' import { persist } from 'zustand/middleware' type CounterStore = { count: number increment: () => void } const useCounterStore = create()( persist( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'counter-storage', skipHydration: true, onRehydrateStorage: () => { console.log('hydration started') return (_state, error) => { if (error) { console.error('hydration failed', error) } else { console.log('hydration finished') } } }, }, ), ) export function Counter() { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) const [hydrated, setHydrated] = useState( useCounterStore.persist.hasHydrated(), ) useEffect(() => { const unsubscribe = useCounterStore.persist.onFinishHydration(() => { setHydrated(true) }) void useCounterStore.persist.rehydrate() return unsubscribe }, []) return (

{hydrated ? `Saved count: ${count}` : 'Loading saved count…'}

) } ``` ![The mounted counter shows the hydration state and the Increment button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/77d909351a7de70b03ee7e57163f93a5.png) On the first render, the component shows `Loading saved count…` and the store still contains `count: 0`. After `rehydrate()` finishes, the finish listener sets `hydrated` to `true`, and the component shows the stored count. The listener returns an unsubscribe function; returning it from the effect removes the listener when the component unmounts. `hasHydrated()` is a non-reactive status check. Read it for the initial local value, then use `onFinishHydration` to update React state when the status changes. `rehydrate()` returns a promise, so you can also await it from application code when the render lifecycle is not the place to start hydration. ## Handle failures Keep failure handling in the returned callback when you need the error and the completion handling in the same option. The callback receives the hydrated state and no error on success; on failure it receives an error instead. In the sample, success and failure are visible in the browser console, while the component's finish listener controls the rendered loading state. If your storage is asynchronous, the initial render can still show the default state until hydration completes. Gate dependent UI with the same completion signal rather than assuming persisted values are present during the first render. For nested persisted objects, see [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) before relying on the default shallow merge. ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) — configure persistence and storage. - [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores) — choose a store lifecycle for server-rendered applications. - [Migrate persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/migrate-persisted-state) — handle stored data from an older version. # Migrate persisted state Use `version` and `migrate` when a breaking state-shape change makes the data already in storage incompatible with the store in your code. The migration runs during rehydration, returns the current state shape, and lets `persist` write the migrated value back to storage. ## When to use this Add a new version when you rename, remove, or otherwise change a persisted field. Keep the same `name` so existing data is found, and make `migrate` handle the stored versions your application still supports. ## Migrate a renamed field 1. Install Zustand in your application: ```bash npm install zustand ``` 2. Set the storage item to the old shape and create the store with a new version. This complete React example renames `oldLayout` to `layout`: ```tsx import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { persist } from 'zustand/middleware' type Layout = 'compact' | 'comfortable' type SettingsStore = { theme: 'light' | 'dark' layout: Layout } type LegacySettings = { theme?: 'light' | 'dark' oldLayout?: Layout } function isLegacySettings(value: unknown): value is LegacySettings { return typeof value === 'object' && value !== null } localStorage.setItem( 'settings', JSON.stringify({ state: { theme: 'dark', oldLayout: 'compact' }, version: 0, }), ) const useSettingsStore = create()( persist( () => ({ theme: 'light', layout: 'comfortable', }), { name: 'settings', version: 1, migrate: (persistedState, version) => { if (version === 0 && isLegacySettings(persistedState)) { return { theme: persistedState.theme ?? 'light', layout: persistedState.oldLayout ?? 'comfortable', } } return { theme: 'light', layout: 'comfortable', } }, }, ), ) function Settings() { const theme = useSettingsStore((state) => state.theme) const layout = useSettingsStore((state) => state.layout) return

{theme} theme, {layout} layout

} const container = document.getElementById('root')! createRoot(container).render( , ) ``` ![The mounted component shows the migrated dark theme and compact layout.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/a9864c6189c5996a13382411ef028665.png) The mounted component reads the migrated state and displays `dark theme, compact layout`. During hydration, Zustand passes the stored state and its stored version (`0`) to `migrate`; the returned state uses the current shape. Because the migration ran, the middleware persists the migrated value under `settings`. 3. In your application, replace the example's seeded `localStorage` value with the data your earlier release wrote. Keep the migration's return value compatible with the current store, and increment `version` again for the next breaking shape change. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | — | Selects the storage key. It is required and must be unique. | | `version` | `number` | `0` | Identifies the current persisted shape. A mismatch with the stored version triggers `migrate`; without a migration function, the stored value is not used. | | `migrate` | `(persistedState: unknown, version: number) => PersistedState \| Promise` | `(persistedState) => persistedState` | Converts the stored state from its recorded version to the current shape. It can return the result synchronously or asynchronously. | ## Pitfalls - `version` is compared with the version stored alongside the state, not inferred from the fields. Change it when the persisted shape changes. - Return the latest persisted shape from `migrate`; do not return the old field name. - A migration runs during hydration. With asynchronous storage, the component can render its initial state before hydration finishes; wait for hydration when that temporary state matters. See [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data). - Persist merges the migrated value with the current state shallowly by default. For partially persisted nested objects, use a custom merge as described in [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state). ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) — configure persistence and storage. - [Persist selected state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-selected-state) — persist only selected fields. - [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) — preserve nested fields during hydration. # Optimize computed selections Use a selector for the value your component needs, and wrap a selector that creates a new object or array with [`useShallow`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-react-shallow#useshallow) when its top-level values can stay the same. ## When to use it Use atomic selectors when you need individual values. Use `useShallow` when one component needs several values or a computed collection such as `Object.keys(state)`. Zustand compares selector results with `Object.is` by default, so a newly created object or array is different even when its contents are unchanged. Install Zustand and React before using either React entry point: ```bash npm install zustand react ``` ## Select individual values Create the store with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create), then select each primitive or action separately: ```tsx import { create } from 'zustand' type SearchStore = { searchValue: string setSearchValue: (value: string) => void } const useSearchStore = create((set) => ({ searchValue: '', setSearchValue: (value) => set({ searchValue: value }), })) export function SearchBox() { const searchValue = useSearchStore((state) => state.searchValue) const setSearchValue = useSearchStore((state) => state.setSearchValue) return ( ) } ``` ![The mounted search field is visible.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/98553dc230d2ecb83f6d8304398cc448.png) The component receives the current string and the action without constructing a wrapper object. It re-renders when either selected value changes. ## Keep a computed selection stable When the selector constructs an object or array, pass it through `useShallow`: ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type MealStore = { papaBear: string mamaBear: string babyBear: string } const useMealStore = create(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) export function BearNames() { const names = useMealStore( useShallow((state) => Object.keys(state)), ) return
{names.join(', ')}
} ``` ![The mounted component displays the three bear names.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/98553dc230d2ecb83f6d8304398cc448.png) `useShallow` returns a memoized selector. The component displays `papaBear, mamaBear, babyBear`, and changing a meal value does not change that array's top-level contents, so it does not cause an unnecessary render of `BearNames`. Adding or removing a store key changes the displayed names and causes the component to update. The same pattern applies to several selected values. This complete component returns one stable object: ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type SearchStore = { searchValue: string setSearchValue: (value: string) => void } const useSearchStore = create((set) => ({ searchValue: 'porridge', setSearchValue: (value) => set({ searchValue: value }), })) export function SearchSummary() { const selection = useSearchStore( useShallow((state) => ({ searchValue: state.searchValue, setSearchValue: state.setSearchValue, })), ) return (
{selection.searchValue}
) } ``` The returned object keeps its previous reference when its selected properties are shallow-equal. If you use an array, object, `Map`, or `Set`, the comparison is shallow: nested values are compared by reference rather than recursively. ## Compare values explicitly Use [`shallow`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-shallow#shallow) when you need a boolean comparison outside a store subscription: ```tsx import { shallow } from 'zustand/shallow' const previous = { firstName: 'John', lastName: 'Doe' } const next = { firstName: 'John', lastName: 'Doe' } const unchanged = shallow(previous, next) export function ComparisonResult() { return {String(unchanged)} } ``` ![The mounted comparison result displays true.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/82e56ad551f99be6cd32f1992c660d15.png) `unchanged` is `true` because the top-level properties match. A nested object with a different reference makes the comparison `false` even when its nested contents look identical. ## Use a custom equality function when shallow comparison is not enough For a store that needs a custom equality function, use [`createWithEqualityFn`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-traditional#createwithequalityfn) from `zustand/traditional`: ```tsx import { createWithEqualityFn } from 'zustand/traditional' type ResultStore = { result: { value: number; label: string } } const useResultStore = createWithEqualityFn(() => ({ result: { value: 1, label: 'one' }, })) const sameResult = (left: ResultStore['result'], right: ResultStore['result']) => left.value === right.value && left.label === right.label export function Result() { const result = useResultStore((state) => state.result, sameResult) return {result.label}: {result.value} } ``` The component uses `sameResult` for this selection, so it updates only when that comparison returns `false`. The `zustand/traditional` entry point requires the `use-sync-external-store` peer dependency; install it with the package: ```bash npm install zustand react use-sync-external-store ``` ## Avoid unstable selector loops In v5, this selector creates a new array on every render: ```tsx type SearchStore = { searchValue: string setSearchValue: (value: string) => void } const unstableSelector = (state: SearchStore) => [ state.searchValue, state.setSearchValue, ] ``` Because the default comparison is `Object.is`, the new reference can trigger a maximum-update-depth loop. Replace it with the `useShallow` version above, or use two atomic selectors. Do not return a fallback function created inside a selector; define the fallback once so the selector returns a stable reference. ## Related - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) # Create a vanilla store Create a framework-independent store, read it from React with `useStore`, and keep one scoped instance stable for each provider. ## When to use a vanilla store Use a vanilla store when store creation must not depend on React—for example, when non-React code also reads or updates the state, or when each rendered scope needs its own instance. Install the [`zustand`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand) package, which includes the [`zustand/vanilla`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-vanilla) entry point: ```bash npm install zustand ``` ## Create the store Call [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) from the [`zustand/vanilla`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-vanilla) entry point when the store must be created independently of React; for the general store API and update model, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works). ```ts import { createStore } from 'zustand/vanilla' type CounterState = { count: number increment: () => void } export const counterStore = createStore()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) counterStore.getState().increment() const currentCount = counterStore.getState().count const unsubscribe = counterStore.subscribe((state) => { console.log(state.count) }) unsubscribe() ``` These reads and subscriptions are the vanilla-store boundary described in [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works); use `getInitialState()` when an external renderer needs an initial render before it subscribes. For immutable updates and shallow merging, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works). Do not assume that a middleware-modified `set` or `get` also changes a vanilla store's `setState` or `getState`; those store methods do not include that middleware behavior. ## Bind the store to React Pass the store and a selector to [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore). The component reads the selected value and re-renders when that selection changes. ```tsx import { useEffect } from 'react' import { createStore } from 'zustand/vanilla' import { useStore } from 'zustand' type CounterState = { count: number increment: () => void } const counterStore = createStore()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const count = useStore(counterStore, (state) => state.count) return (

Count: {count}

) } function ExternalIncrement() { useEffect(() => { const button = document.getElementById('external-increment') if (!(button instanceof HTMLButtonElement)) return const increment = () => counterStore.getState().increment() button.addEventListener('click', increment) return () => button.removeEventListener('click', increment) }, []) return ( ) } export default function App() { return ( <> ) } ``` ![The mounted counter shows Count: 0 and an external increment button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/9ab651969e5ec9a42a204ba49d752463.png) The mounted counter starts at `Count: 0`. Clicking the separate button calls the vanilla store directly through `getState()`, while `useStore` updates the React display; the store remains usable outside the component tree. ## Keep scoped instances stable Create a store factory when a component tree needs independent state. Keep the instance in React state with a lazy initializer; creating it during every render would replace the store and lose its state and subscriptions. ```tsx import { createContext, useContext, useState, type ReactNode } from 'react' import { createStore } from 'zustand/vanilla' import { 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 useCounterStore(selector: (state: CounterState) => U): U { const store = useContext(CounterContext) if (store === null) { throw new Error('useCounterStore must be used within CounterProvider') } return useStore(store, selector) } function Counter() { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) return ( ) } export default function App() { return (
) } ``` ![Two buttons show independent counts, Count: 2 and Count: 10.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/424bfa87a15fe5d592cc96db23e9ea42.png) The page shows two buttons with independent counts, `Count: 2` and `Count: 10`. Each provider creates its store once, so clicking one button changes only its own count. The lazy `useState` initializer also keeps the store identity stable across re-renders. For server-rendered applications, create stores per request rather than sharing a global module store; see [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores). ## API summary | API | Use it for | | --- | --- | | `createStore` | Create a store without a framework binding. | | `useStore` | Select state from a vanilla store in a React component. | | `StoreApi` | Type the store instance that provides `setState`, `getState`, `getInitialState`, and `subscribe`. | See [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores) for the store model and [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) for reusable TypeScript shapes. # Compose store middleware Use shipped middleware around one store creator when the store needs persistence, Redux DevTools inspection, Immer updates, inferred slices, or Redux-style dispatching. ## When to use this Compose middleware at store creation time. The outer middleware receives the state creator returned by the inner middleware, so the order is part of the store's type and behavior. A practical React store can persist its state, label updates in Redux DevTools, and update nested data with Immer without adding a provider. Install the core package and the optional middleware dependencies: ```bash npm install zustand immer @redux-devtools/extension ``` The Redux DevTools browser extension is also required for the DevTools connection. ## Compose a React store Define the store once, then render a component that selects the state and action it needs. This example shows the difference composition makes: one mounted todo list gets persistence, Redux DevTools action labels, and draft-style updates from a single store. For the typed store setup and the individual API roles, see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns). The component renders a todo list; clicking **Add todo** appends a todo, and clicking a todo toggles its completion. ```tsx title="src/main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' type Todo = { text: string done: boolean } type TodoStore = { todos: Todo[] addTodo: (text: string) => void toggleTodo: (index: number) => void } const useTodoStore = create()( devtools( persist( immer((set) => ({ todos: [{ text: 'Read the store', done: false }], addTodo: (text) => set((state) => { state.todos.push({ text, done: false }) }, false, 'todos/add'), toggleTodo: (index) => set((state) => { const todo = state.todos[index] if (todo) todo.done = !todo.done }, false, 'todos/toggle'), })), { name: 'todo-store' }, ), { name: 'TodoStore' }, ), ) function TodoList() { const todos = useTodoStore((state) => state.todos) const addTodo = useTodoStore((state) => state.addTodo) return (

Todos

    {todos.map((todo, index) => (
  • ))}
) } const root = document.getElementById('root')! createRoot(root).render( , ) ``` `immer` makes the updater receive a draft, so `push` and property assignment produce immutable state updates. `persist` stores the serializable state under `todo-store` and rehydrates it when the store starts. `devtools` connects the store named `TodoStore`; the third argument passed to `set` labels each update as `todos/add` or `todos/toggle`. The mounted component displays the current list, and selecting `todos` makes it re-render when that list changes. The order above follows the repository's typed composition pattern: `devtools(persist(immer(...)))`. Keep the action label's second argument as `false` when supplying the third argument; that preserves the normal merge behavior. ## Add inferred state with `combine` Use [`combine`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#combine) when an initial object and the actions that extend it are easier to read than a repeated state type. It returns a state creator, so pass it directly to `create`: ```tsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useCounterStore = create( combine({ count: 0 }, (set, get) => ({ increment: () => set({ count: get().count + 1 }), })), ) export function Counter() { const count = useCounterStore((state) => state.count) const increment = useCounterStore((state) => state.increment) return } ``` The returned hook has inferred `count` and `increment` fields. `combine` is an alternative to writing the store type explicitly; it does not replace the middleware composition used when the store also needs persistence or inspection. ## Use a reducer and `dispatch` Use [`redux`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#redux) when updates already fit a reducer and action objects. The middleware places `dispatch` in the store, so a component still selects from a Zustand hook: ```tsx import { create } from 'zustand' import { createRoot } from 'react-dom/client' import { redux } from 'zustand/middleware' type CounterState = { count: number } type CounterAction = | { type: 'increment' } | { type: 'decrement' } const counterReducer = ( state: CounterState, action: CounterAction, ): CounterState => { if (action.type === 'increment') return { count: state.count + 1 } return { count: state.count - 1 } } const useReduxCounter = create( redux(counterReducer, { count: 0 }), ) function ReduxCounter() { const count = useReduxCounter((state) => state.count) const dispatch = useReduxCounter((state) => state.dispatch) return ( ) } document.body.innerHTML = '
' const root = document.getElementById('root')! createRoot(root).render() ``` The button dispatches an action, and the reducer returns the next complete `CounterState`. Wrap this creator with `devtools` when you also want action inspection. ## Observe selected changes outside React Use [`subscribeWithSelector`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#subscribewithselector) when a vanilla subscription needs a selector rather than every state change. Its listener receives the selected value and its previous value: ```ts import { createStore } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' const store = createStore( subscribeWithSelector(() => ({ status: 'idle' as 'idle' | 'ready' })), ) const unsubscribe = store.subscribe( (state) => state.status, (status, previousStatus) => { console.log(previousStatus, '->', status) }, ) store.setState({ status: 'ready' }) unsubscribe() ``` The listener runs when the selected `status` changes, not for unrelated fields. Supply `fireImmediately: true` or an `equalityFn` in the third argument when that subscription needs those semantics. ## Options that matter | Option | Type | Default | What it does | |---|---|---|---| | `persist.name` | `string` | — | Names the stored value; use a unique name for each persisted store. | | `persist.storage` | `PersistStorage` | JSON storage backed by `window.localStorage` | Selects the persistence engine. | | `persist.partialize` | `(state) => persistedState` | — | Filters the state written to storage. | | `persist.skipHydration` | `boolean` | `false` | Prevents hydration during initialization so the application can call `rehydrate()` at a controlled point. | | `devtools.name` | `string` | — | Names the connection in Redux DevTools. | | `devtools.enabled` | `boolean` | development: `true`, production: `false` | Enables or disables the DevTools integration. | | `devtools.anonymousActionType` | `string` | inferred action type or `anonymous` | Names mutations that do not provide an action type. | For a nonstandard storage engine, use [`createJSONStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#createjsonstorage) with a [`StateStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#statestorage) implementation. The `storage` option accepts [`PersistStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persiststorage). If persisted storage is asynchronous, hydration happens after the initial render; use the hydration controls described in [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data). Persist's default merge is shallow, so use [Merge persisted state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/merge-persisted-state) for nested persisted data that needs a deep merge. ## Pitfalls - Add `immer` and `@redux-devtools/extension` when using those middleware; `zustand` alone does not install either dependency. - Keep middleware around the rendered store creator. A standalone middleware call does not update a store that a component renders. - Give each persisted store a unique `name`; the value identifies its storage entry. - Use `useShallow` or a stable selector when a selector returns a new reference on every render. See [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). ## Live demo See the [Zustand live demo](https://zustand-demo.pmnd.rs/) for a running store-driven interface. ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) - [Update nested state with Immer](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer) - [Use Redux-style reducers](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/use-redux-style-reducers) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) # Update nested state with Immer Use [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with [`immer`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware-immer#immer) when an action changes several levels of an object or a collection. You write the update with draft-mutation syntax; the middleware produces the immutable state that Zustand passes to its subscribers. ## When to use it Use Immer when spreading every level of a nested value makes an action hard to read. Zustand's normal update is a shallow merge, so replacing a nested object without copying its existing properties can discard sibling fields. For a single flat field, a regular `set` update remains enough. Install both packages in your application: ```bash npm install zustand immer ``` ## Update nested objects and a collection Define the state and actions together. The `immer` wrapper changes the `set` callback so its `state` argument is an Immer draft. ```tsx import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' type Profile = { name: string preferences: { theme: 'light' | 'dark' notifications: boolean } } type Todo = { id: string title: string done: boolean } type Store = { profile: Profile todos: Todo[] setTheme: (theme: Profile['preferences']['theme']) => void toggleTodo: (id: string) => void } export const useStore = create()( immer((set) => ({ profile: { name: 'Ada Lovelace', preferences: { theme: 'light', notifications: true, }, }, todos: [ { id: 'learn-zustand', title: 'Learn Zustand', done: false }, { id: 'write-example', title: 'Write an example', done: false }, ], setTheme: (theme) => set((state) => { state.profile.preferences.theme = theme }), toggleTodo: (id) => set((state) => { const todo = state.todos.find((item) => item.id === id) if (todo) { todo.done = !todo.done } }), })), ) export function App() { const name = useStore((state) => state.profile.name) const theme = useStore((state) => state.profile.preferences.theme) const todos = useStore((state) => state.todos) const setTheme = useStore((state) => state.setTheme) const toggleTodo = useStore((state) => state.toggleTodo) return (

{name}

Theme: {theme}

    {todos.map((todo) => (
  • ))}
) } ``` ![The rendered profile name, theme control, and two todo items appear.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/b595adc48d78741c1628b36d069e89c1.png) The mounted component displays the profile name, the current theme, and both todos. Clicking **Toggle theme** changes only `profile.preferences.theme`; the other profile fields remain present. Clicking a todo changes that item's `done` value and updates its label from `Open` to `Done` (or back). The selectors subscribe to the values they read, so the component receives the produced state through the store hook. ## Keep collection references observable Do not mutate a `Map` or `Set` in place in a regular Zustand update. An in-place mutation keeps the same collection reference, so Zustand may treat the selected value as unchanged. Create a new `Map` or `Set` for each update instead. The array update above uses Immer draft syntax, which produces the immutable collection state for the changed item. ## Watch the Immer edge cases Immer must proxy the value you mutate. If you mutate a class object without marking it `[immerable] = true`, Immer can change the current object without producing a new proxied state. Zustand can then see equal current and next state and skip subscriptions. Mark such class objects as Immerable and follow Immer's rules. ## See it running Open the [nested-state demo](https://stackblitz.com/edit/vitejs-vite-j6bjdygu) to see the rendered result in a browser. For the middleware's standalone examples, see the [basic](https://stackblitz.com/edit/vitejs-vite-3sgc4ejy) and [advanced](https://stackblitz.com/edit/vitejs-vite-jxxtuyj3) demos. For selector stability and shallow comparison, continue with [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). For a vanilla store, see [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store). # Handle server-rendered stores Use a store factory and a client-side provider when a React server-rendered application needs state that is isolated per request and identical during server rendering and client hydration. ## When to use this pattern Use this pattern when a server can handle concurrent requests or when the initial state comes from request data. A module-level store is shared by requests, so one request can observe another request's state. Create the store inside the provider instead, and pass the same serializable initial state to the server-rendered tree and the browser. React Server Components must not read from or write to the store. Keep store access in the client component that consumes the provider; a server component can provide initial data as props. ## 1. Create a store factory Build the store with [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore), not as a module-level singleton. The factory accepts the state for one request and creates actions that update that store. ```ts title="src/stores/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 })), })) ``` Each call to `createCounterStore()` returns a separate vanilla store, so each provider can keep state scoped to its own request or route. See [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) for the returned store API; the component below uses its React binding instead of reading the store directly. ## 2. Create the provider once per render tree Create the store in a lazy `useState` initializer. The initializer runs once for the provider instance, so a client re-render does not replace the store. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) subscribes a component to the store and selects the requested value. The [`StoreApi`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storeapi) type describes the store API held by the context. ```tsx title="src/providers/counter-store-provider.tsx" 'use client' import { createContext, useContext, useState, type ReactNode } from 'react' import { useStore, type StoreApi } from 'zustand' import { createStore } from 'zustand/vanilla' type CounterState = { count: number } type CounterStore = CounterState & { decrementCount: () => void incrementCount: () => void } type CounterStoreApi = StoreApi const CounterStoreContext = createContext( undefined, ) export type CounterStoreProviderProps = { children: ReactNode initialState: CounterState } export function CounterStoreProvider({ children, initialState, }: CounterStoreProviderProps) { const [store] = useState(() => createStore()((set) => ({ ...initialState, decrementCount: () => set((state: CounterStore) => ({ count: state.count - 1 })), incrementCount: () => set((state: CounterStore) => ({ count: state.count + 1 })), })), ) const content = children ??

Count: {store.getState().count}

return ( {content} ) } export function useCounterStore( selector: (state: CounterStore) => T, ): T { const store = useContext(CounterStoreContext) if (store === undefined) { throw new Error( 'useCounterStore must be used within CounterStoreProvider', ) } return useStore(store, selector) } ``` The provider owns one store for its mounted subtree. Rendering a second provider creates a second store, which is useful when separate parts of an application need separate request or route scopes. ## 3. Read and update state in a client component Mark the component that calls the custom hook as a client component. It renders the request's initial count on both server and client, then updates that count when the user clicks a button. ```tsx title="src/components/home-page.tsx" 'use client' import { useCounterStore } from '@/providers/counter-store-provider' type CounterStore = { count: number incrementCount: () => void decrementCount: () => void } export function HomePage() { const count = useCounterStore((state: CounterStore) => state.count) const incrementCount = useCounterStore( (state: CounterStore) => state.incrementCount, ) const decrementCount = useCounterStore( (state: CounterStore) => state.decrementCount, ) return (

Count: {count}

) } ``` The page initially shows `Count: 3` in this example. Clicking **Increment Count** changes it to `4`; clicking **Decrement Count** changes it to `2` when starting from `3`. Each selector subscribes only to the selected value. ## 4. Pass matching initial state from the server In the Next.js App Router, place the provider in the server-rendered layout and pass serializable request data to it. The page below represents request-derived data with a fixed value; replace that value with data loaded for the current request, without reading or writing the Zustand store in the server component. ```tsx title="src/app/layout.tsx" import type { ReactNode } from 'react' import { CounterStoreProvider } from '@/providers/counter-store-provider' export default function RootLayout({ children }: { children: ReactNode }) { const initialState = { count: 3 } return ( {children} ) } ``` ```tsx title="src/app/page.tsx" import { HomePage } from '@/components/home-page' export default function Page() { return } ``` The server output and the client's first render both use `count: 3`, so React hydrates matching markup. After hydration, the buttons update the client-side store. If the initial state differs between those renders, React can report a hydration error. For the Pages Router, place `CounterStoreProvider` around `Component` in `src/pages/_app.tsx`. If only one route needs the store, place the provider in that route instead; that gives the route its own store scope. ## Options that matter This pattern has no Zustand configuration options. The values that determine its behaviour are the factory's `initState`, the provider's `initialState`, and each selector passed to `useCounterStore`. ## Pitfalls - Do not define the store as a module-level variable. A global store can leak state between concurrent server requests. - Do not read or write the store from a React Server Component. Pass request data into a client provider instead. - Keep the server's initial state and the client's initial state identical. Rendering different data produces hydration errors. - Keep the provider's store creation inside the lazy `useState` initializer. Creating it during every render replaces the instance when the provider re-renders. For persisted stores, asynchronous storage hydrates after the initial render; see [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) and [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration). For the distinction between initializer updates and the direct vanilla-store API, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) and [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store). ## Related - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) - [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores) - [React hydration](https://react.dev/reference/react-dom/client/hydrateRoot) # Subscribe to store state outside components Use a vanilla store when code outside React needs to read current state, update it, or react to changes. Bind that same store to React with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore), and use a selected subscription when an external listener only needs part of the state. ## Create a store for non-React code For [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore), [`StoreApi`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storeapi), and their `getState()`, `setState()`, `getInitialState()`, and `subscribe()` methods, see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns); this page focuses on using them from non-React code. The subscription can update a browser element directly, so an external listener changes the page without a React render: ```ts title="counter-store.ts" import { createStore } from 'zustand/vanilla' type CounterStore = { count: number increment: () => void } export const counterStore = createStore()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })) const output = document.createElement('output') const button = document.createElement('button') button.type = 'button' button.textContent = 'Increment' const render: Parameters[0] = (state) => { output.textContent = `Count: ${state.count}` } render(counterStore.getInitialState(), counterStore.getInitialState()) counterStore.subscribe(render) button.addEventListener('click', () => counterStore.getState().increment()) document.body.append(output, button) ``` Read and update the store from any code that imports it. A subscription callback receives the new state and the previous state, and the returned function removes that listener. ```ts title="counter-effects.ts" import { counterStore } from './counter-store' const initialCount = counterStore.getInitialState().count const currentCount = counterStore.getState().count const unsubscribe = counterStore.subscribe((state, previousState) => { console.log('count changed', previousState.count, state.count) }) counterStore.getState().increment() counterStore.setState({ count: currentCount + 2 }) console.log('initial count', initialCount) unsubscribe() ``` The first read gets the store's initial model; later `getState()` calls get the current model. The action updates state through the store, and `setState()` performs the update without requiring a component. ## Subscribe to a selected value Wrap the state creator with [`subscribeWithSelector`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#subscribewithselector) when an external listener needs a slice rather than every state update. ```ts title="temperature-store.ts" import { createStore } from 'zustand/vanilla' import { subscribeWithSelector } from 'zustand/middleware' type TemperatureStore = { celsius: number label: string setTemperature: (celsius: number) => void } export const temperatureStore = createStore()( subscribeWithSelector((set) => ({ celsius: 20, label: 'room', setTemperature: (celsius) => set({ celsius }), })), ) const unsubscribe = temperatureStore.subscribe( (state) => state.celsius, (celsius, previousCelsius) => { console.log('temperature changed', previousCelsius, celsius) }, { fireImmediately: true }, ) temperatureStore.setState({ label: 'office' }) temperatureStore.getState().setTemperature(21) unsubscribe() ``` The listener fires immediately with the selected value and then fires when `celsius` changes. The `label` update does not call this listener because the selected value is unchanged. The selector subscription compares selected values with `Object.is` unless you provide `equalityFn` in its options. ## Use the same store in React Pass the vanilla store to `useStore` and select the value or action the component needs. React subscribes the component to that selection; the component re-renders when the selected value changes. ```tsx title="TemperaturePanel.tsx" import { createRoot } from 'react-dom/client' import { useStore } from 'zustand' import { temperatureStore } from './temperature-store' export function TemperaturePanel() { const celsius = useStore(temperatureStore, (state) => state.celsius) const setTemperature = useStore( temperatureStore, (state) => state.setTemperature, ) return (

Temperature: {celsius}°C

) } const root = document.createElement('div') document.body.append(root) createRoot(root).render() ``` The panel displays the current temperature. Clicking **Increase** updates the shared vanilla store, and the selected `celsius` value causes the panel to render the new temperature. ## Handle high-frequency changes without re-rendering For frequently changing state, subscribe in an effect and update a DOM ref directly. The subscription changes the rendered element without making the component render for every pointer event. ```tsx title="PointerPreview.tsx" import { useEffect, useRef } from 'react' import { createStore } from 'zustand/vanilla' type PointerStore = { x: number y: number setPosition: (x: number, y: number) => void } const pointerStore = createStore()((set) => ({ x: 0, y: 0, setPosition: (x, y) => set({ x, y }), })) export function PointerPreview() { const dotRef = useRef(null) useEffect(() => { const unsubscribe = pointerStore.subscribe((state) => { const dot = dotRef.current if (dot) { dot.style.transform = `translate(${state.x}px, ${state.y}px)` } }) return unsubscribe }, []) return (
pointerStore.getState().setPosition(event.clientX, event.clientY) } style={{ position: 'relative', height: 240, width: 400 }} >
) } ``` The red dot moves with the pointer while the pointer is over the 400-by-240 pixel area. The effect subscribes when the component mounts and removes the listener when it unmounts, so the component does not retain a subscription after teardown. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `equalityFn` | `(a, b) => boolean` | `Object.is` | Decides whether a selected-value listener runs. | | `fireImmediately` | `boolean` | `false` | Calls a selected-value listener once with its current value when the subscription is created. | ## Pitfalls - Keep the unsubscribe function and call it when the owner of the subscription is disposed. In a React component, return it from the `useEffect` cleanup as shown above. - A selector that creates a new reference on every render can cause update loops in v5. Return a stable reference or follow the shallow-selection guidance in [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). - Do not read or write a global store from React Server Components. Create stores per request for server-rendered applications; see [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores). ## Related - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) # Reset store state Use a reset action when a screen needs to return a store to its initial values without removing the actions defined on that store. ## Reset a React store Define the reset action beside the state. [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) gives you a React hook with the store API attached, so the component can select the values and the reset action from the mounted store. ```tsx import { create } from 'zustand' type FormState = { name: string email: string } type FormActions = { setName: (name: string) => void setEmail: (email: string) => void reset: () => void } type FormStore = FormState & FormActions const useFormStore = create()((set, _get, store) => ({ name: '', email: '', setName: (name) => set({ name }), setEmail: (email) => set({ email }), reset: () => set(store.getInitialState()), })) export default function App() { const name = useFormStore((state) => state.name) const email = useFormStore((state) => state.email) const setName = useFormStore((state) => state.setName) const setEmail = useFormStore((state) => state.setEmail) const reset = useFormStore((state) => state.reset) return (
{ event.preventDefault() }} >
) } ``` The `reset` action calls `set(store.getInitialState())`; the default shallow merge also retains `setName`, `setEmail`, and `reset`. `getInitialState()` returns the state captured when the store was created. ## Reset a vanilla store used by React Use [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) when the store must exist independently of React. For its standalone API and React binding, see [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores). For this reset pattern, keep the `reset` action in the state creator and bind the store to React with [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore). ```tsx title="App.tsx" import { createStore } from 'zustand/vanilla' import { useStore } from 'zustand' type FormStore = { name: string email: string setName: (name: string) => void setEmail: (email: string) => void reset: () => void } export const formStore = createStore()((set, _get, store) => ({ name: '', email: '', setName: (name) => set({ name }), setEmail: (email) => set({ email }), reset: () => set(store.getInitialState()), })) export default function App() { const name = useStore(formStore, (state) => state.name) const email = useStore(formStore, (state) => state.email) const setName = useStore(formStore, (state) => state.setName) const setEmail = useStore(formStore, (state) => state.setEmail) const reset = useStore(formStore, (state) => state.reset) return (
setName(event.target.value)} /> setEmail(event.target.value)} />
) } ``` Use `useStore` to bind the standalone `formStore` to React, and use its API methods from non-React code. ## Reset several stores Keep one reset function per store in a set, then invoke them from a single `resetAllStores` action. For a complete replacement, pass `true` as the second argument to `setState`, as in the authors' pattern. The [`StateCreator`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#statecreator) type describes the function passed to the store creator: ```ts import type { StateCreator } from 'zustand' import { create as actualCreate } from 'zustand' const storeResetFns = new Set<() => void>() export const resetAllStores = () => { storeResetFns.forEach((reset) => reset()) } export const create = (() => { return (stateCreator: StateCreator) => { const store = actualCreate(stateCreator) storeResetFns.add(() => { store.setState(store.getInitialState(), true) }) return store } }) as typeof actualCreate ``` The wrapper registers each created store, and `resetAllStores()` calls every registered reset function. The `true` replace flag replaces the complete state rather than shallow-merging it. ## Pitfalls - Do not call `setState(initialState, true)` for a partial state model: replacement discards properties not present in that value. Use the default merge for the colocated reset action, or include the complete initial state when replacing. - If a store is global in a server-rendered application, use a per-request store instead; see [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores). ## Live demo Try the [basic reset-state demo](https://stackblitz.com/edit/zustand-how-to-reset-state-basic) to see the React pattern mounted in a browser. The [advanced demo](https://stackblitz.com/edit/zustand-how-to-reset-state-advanced) shows the multi-store pattern. ## Related - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) - [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates) # Sync state with a URL hash Use [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with a custom [`StateStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#statestorage) adapter and [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) when a shareable URL must restore selected Zustand state. This example stores a fish count in the URL hash and renders the count in React. ## When to use this Use this pattern for small, shareable state such as a filter, selected tab, or view setting. The storage adapter keeps the URL representation separate from the store; `persist` still performs serialization and rehydration. ## Store the state in the hash Create the hash adapter, then pass it through [`createJSONStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#createjsonstorage). The `name` is the key inside the hash, so choose a unique name for the store. ```tsx title="src/fish-store.ts" import { create } from 'zustand' import { createJSONStorage, persist, type StateStorage, } from 'zustand/middleware' type FishStore = { fishes: number addAFish: () => void } const hashStorage: StateStorage = { getItem: (key) => { const searchParams = new URLSearchParams(window.location.hash.slice(1)) return searchParams.get(key) }, setItem: (key, value) => { const searchParams = new URLSearchParams(window.location.hash.slice(1)) searchParams.set(key, value) window.location.hash = searchParams.toString() }, removeItem: (key) => { const searchParams = new URLSearchParams(window.location.hash.slice(1)) searchParams.delete(key) window.location.hash = searchParams.toString() }, } export const useFishStore = create()( persist( (set, get) => ({ fishes: 0, addAFish: () => set({ fishes: get().fishes + 1 }), }), { name: 'fish-store', storage: createJSONStorage(() => hashStorage), }, ), ) ``` `createJSONStorage` writes the persisted envelope as JSON. After an update, the hash contains an encoded `fish-store` value with the state and its persistence version. A new page load reads that value before the component displays the restored count. ## Render and update it in React Read the state and action through the store hook. Mounting this component gives you a visible count and a button; clicking the button updates both the count and the URL hash. ```tsx title="src/App.tsx" import { useFishStore } from './fish-store' export function App() { const fishes = useFishStore((state) => state.fishes) const addAFish = useFishStore((state) => state.addAFish) return (

Fishes: {fishes}

) } ``` The component re-renders when its selected `fishes` value changes. Copy the page URL after adding fish, open it in a new tab, and the store rehydrates the count from the hash. ## Use a query parameter instead A query-string adapter has the same `StateStorage` shape. Use `history.replaceState` in `setItem` so updating the state changes the URL without navigating or refreshing the page. The adapter below reads the store's persisted value from `?fish-store=...` and writes it in place. ```ts title="src/query-storage.ts" import type { StateStorage } from 'zustand/middleware' export const queryStorage: StateStorage = { getItem: (key) => { return new URLSearchParams(window.location.search).get(key) }, setItem: (key, value) => { const searchParams = new URLSearchParams(window.location.search) searchParams.set(key, value) window.history.replaceState( null, '', `${window.location.pathname}?${searchParams.toString()}${window.location.hash}`, ) }, removeItem: (key) => { const searchParams = new URLSearchParams(window.location.search) searchParams.delete(key) const query = searchParams.toString() window.history.replaceState( null, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`, ) }, } ``` Replace the `storage` option in the store with `createJSONStorage(() => queryStorage)`. The rest of the store and component remain unchanged. If the query string is empty, this adapter leaves it empty until the first persisted update. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `name` | `string` | — | Names the persisted value and must be unique. | | `storage` | [`PersistStorage`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persiststorage) | `createJSONStorage(() => window.localStorage)` | Selects the storage implementation used to read, write, and remove persisted state. | | `partialize` | `(state: Object) => Object` | `(state) => state` | Selects which fields go into the URL. | | `version` | `number` | `0` | Identifies the persisted state format. | | `migrate` | `(persistedState: Object, version: number) => Object \| Promise` | `(persistedState) => persistedState` | Converts a persisted value from an older version. | ## Pitfalls - URL values are visible and shareable. Persist only state that is safe to put in a URL. - The URL adapter makes persisted values visible and shareable, so validate untrusted or stale URL data before treating it as store state. See [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) for the `createJSONStorage`, `JSON.parse`, and `JSON.stringify` details. - If you use asynchronous persisted storage elsewhere, hydration can finish after the initial render. See [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data). ## Live demos - [Hash storage demo](https://stackblitz.com/edit/vitejs-vite-9vg24prg) - [Query-parameter demo](https://stackblitz.com/edit/vitejs-vite-hyc97ynf) ## Related - [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) - [Persist selected state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-selected-state) - [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration) - [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores) # Split a store into typed slices Use slices when one store contains several feature areas. Each slice owns a part of the state and its actions, while the spread composition creates one store that components select from. ## When to use this pattern Split a growing store when its state and actions have clear feature boundaries but still need to work together. Keep the slices as state-creator functions; do not create a separate Zustand store for each slice. The application gets one hook, one state model, and cross-feature actions through `get`. ## 1. Define the slice state and actions Start with the complete store type, then type each slice as a part of that store. The [`StateCreator`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#statecreator) generic receives the full store type and returns only the slice it creates. This lets a bear action update fish state and lets a shared action call actions from both slices. ```ts title="stores/types.ts" export type BearSlice = { bears: number addBear: () => void eatFish: () => void } export type FishSlice = { fishes: number addFish: () => void } export type SharedSlice = { addBearAndFish: () => void } export type Store = BearSlice & FishSlice & SharedSlice ``` ```ts title="stores/bearSlice.ts" import type { StateCreator } from 'zustand' import type { BearSlice, Store } from './types' export const createBearSlice: StateCreator = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) ``` ```ts title="stores/fishSlice.ts" import type { StateCreator } from 'zustand' import type { FishSlice, Store } from './types' export const createFishSlice: StateCreator = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) ``` ```ts title="stores/sharedSlice.ts" import type { StateCreator } from 'zustand' import type { SharedSlice, Store } from './types' export const createSharedSlice: StateCreator = (_set, get) => ({ addBearAndFish: () => { get().addBear() get().addFish() }, }) ``` The bear and fish actions update state through `set`. The shared action reads the current store through `get`, so it can reuse actions from other slices. ## 2. Compose one store Call [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) once and spread every slice into its state creator; that single composition is the difference this pattern makes. For the general typed-store use of `create`, see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns). The returned `useBoundStore` hook exposes all three slices as one state model. ```ts title="stores/useBoundStore.ts" import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' import { createSharedSlice } from './sharedSlice' import type { Store } from './types' export const useBoundStore = create()( devtools( persist( (...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), ...createSharedSlice(...args), }), { name: 'bound-store' }, ), { name: 'bound-store' }, ), ) ``` Apply [`persist`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#persist) and [`devtools`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#devtools) around the combined creator, not inside an individual slice. For their general roles and composition inside `create`, see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns); the slice-specific rule is to keep middleware at the boundary of the complete store. `persist` stores the combined state under `bound-store`; `devtools` names the same store in Redux DevTools. The important order is `devtools(persist(...))`: the repository recommends putting `devtools` as far outside the other middleware as possible because it changes `setState`. ## 3. Select slices in React Select individual fields or actions from the single hook. The mounted component below initially shows `0` bears and `0` fish. **Add a bear** changes only the bear count; **Add both** changes both counts in one action. ```tsx title="App.tsx" import { useBoundStore } from './stores/useBoundStore' export function App() { const bears = useBoundStore((state: ReturnType) => state.bears) const fishes = useBoundStore((state: ReturnType) => state.fishes) const addBear = useBoundStore((state: ReturnType) => state.addBear) const addBearAndFish = useBoundStore((state: ReturnType) => state.addBearAndFish) return (

Animals

Bears: {bears}

Fish: {fishes}

) } ``` The selectors keep the component connected to the fields it reads while the actions remain colocated with their state. A slice can therefore grow without changing the component's store import or creating a provider. ## Options that matter here | Option | Type | Default | What it does | |---|---|---|---| | `name` | `string` | Not specified | Names the persisted entry and the Redux DevTools connection in this example. | ## Pitfalls - Apply middleware only to the combined creator, not inside a slice; see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) for the general middleware pattern. - Do not call `get` while the initial state creator is running. Use it from a cross-slice action after the store exists; see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) for the general creator and initializer rules. - Select stable values from the store. A selector that creates a new reference on every render can cause an infinite update loop in v5; see [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). ## Related - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) - [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) - [Compose store middleware](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/compose-store-middleware) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) # Use Redux-style reducers Use [`redux`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware#redux) when your state changes already fit an action-type and reducer model; it adds a typed `dispatch` function to a Zustand store. ## When to use it Prefer colocated store actions for ordinary Zustand updates. Choose a reducer when a defined set of action types, a pure transition function, or an existing Redux-style workflow makes the update logic clearer. The reducer receives the current state and an action, then returns the next state. ## Create a store with a reducer 1. Define the state and action union. Give each action a `type` and only the payload that action needs. 2. Return a new state from each reducer case. Return the existing state for actions the reducer does not handle. 3. Pass the reducer and initial state to `redux`, then wrap the result with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create). 4. Select `count` and `dispatch` in the component. The sample mounts `Counter` into a page element, where it displays the count and changes it through dispatched actions. ```tsx import { create } from 'zustand' import { redux } from 'zustand/middleware' import { createRoot } from 'react-dom/client' type CounterState = { count: number } type CounterAction = | { type: 'counter/increment'; amount: number } | { type: 'counter/decrement'; amount: number } | { type: 'counter/reset' } const initialState: CounterState = { count: 0, } function counterReducer( state: CounterState, action: CounterAction, ): CounterState { switch (action.type) { case 'counter/increment': return { ...state, count: state.count + action.amount } case 'counter/decrement': return { ...state, count: state.count - action.amount } case 'counter/reset': return initialState default: return state } } const useCounterStore = create( redux(counterReducer, initialState), ) function Counter() { const count = useCounterStore((state) => state.count) const dispatch = useCounterStore((state) => state.dispatch) return (

Count: {count}

) } document.body.innerHTML = '
' const rootElement = document.getElementById('root')! createRoot(rootElement).render() ``` The rendered count starts at `0`. Clicking **Increment** or **Decrement** dispatches an action, the reducer returns the next state, and the selected count updates. **Reset** returns the initial state. ## Use a vanilla store When React is not the store's consumer, use [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore) with the same state creator. The returned store exposes `dispatch` on its state and API, so non-React code can dispatch actions directly. ```ts import { createStore } from 'zustand/vanilla' import { redux } from 'zustand/middleware' type CounterState = { count: number } type CounterAction = | { type: 'counter/increment'; amount: number } | { type: 'counter/reset' } type CounterStore = CounterState & { dispatch: (action: CounterAction) => CounterAction } const initialState: CounterState = { count: 0 } function counterReducer( state: CounterState, action: CounterAction, ): CounterState { switch (action.type) { case 'counter/increment': return { ...state, count: state.count + action.amount } case 'counter/reset': return initialState default: return state } } const counterStore = createStore()( redux(counterReducer, initialState), ) const returnedAction = counterStore.dispatch({ type: 'counter/increment', amount: 2, }) console.log(returnedAction.type) console.log(counterStore.getState().count) ``` `counterStore.dispatch()` returns the action it receives. `counterStore.getState()` then exposes the updated state to non-React code. ## Keep the reducer predictable - Keep the reducer pure: calculate the next state from its two arguments and return it. - Update state immutably. Spread the existing object when changing one field, and return the current `state` for an unhandled action. - Keep action types discriminated with a string `type`; the middleware requires that shape. - Remember that the reducer pattern is optional in Zustand. For smaller stores, define actions directly in the store and update through `set`. For selector reference stability and render behaviour, see [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). For immutable update details, see [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates). Try the [live Zustand demo](https://zustand-demo.pmnd.rs/) to see a mounted store in the browser. ## Related - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) - [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) # Update Maps and Sets immutably Use this when a Zustand state field is a `Map` or `Set` and an update must appear in subscribed React components. Create a new collection for each update; do not mutate the collection already in the store. ## Use new references for collection updates [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) returns a hook that reads selected state and exposes the actions in the same store. Define the collection types explicitly, then copy the existing collection before changing the copy: ```tsx import { create } from 'zustand' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' type InventoryStore = { stock: Map selected: Set receive: (sku: string, quantity: number) => void toggleSelected: (sku: string) => void removeSku: (sku: string) => void } const useInventoryStore = create((set) => ({ stock: new Map([ ['apple', 12], ['orange', 8], ]), selected: new Set(['apple']), receive: (sku, quantity) => set((state) => ({ stock: new Map(state.stock).set( sku, (state.stock.get(sku) ?? 0) + quantity, ), })), toggleSelected: (sku) => set((state) => { const next = new Set(state.selected) if (next.has(sku)) { next.delete(sku) } else { next.add(sku) } return { selected: next } }), removeSku: (sku) => set((state) => { const stock = new Map(state.stock) stock.delete(sku) const selected = new Set(state.selected) selected.delete(sku) return { stock, selected } }), })) export function Inventory() { const stock = useInventoryStore((state) => state.stock) const selected = useInventoryStore((state) => state.selected) const receive = useInventoryStore((state) => state.receive) const toggleSelected = useInventoryStore((state) => state.toggleSelected) const removeSku = useInventoryStore((state) => state.removeSku) return (

Inventory

    {Array.from(stock, ([sku, quantity]) => (
  • {sku}: {quantity}{' '} {' '} {' '}
  • ))}

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

) } const root = document.getElementById('root') if (root) { createRoot(root).render( , ) } ``` ![The mounted inventory shows stock quantities, selection state, and update controls.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/7487d5a8666c6c460b1427cfa205aa23.png) `receive` creates a new `Map`, changes one entry, and stores that reference. `toggleSelected` and `removeSku` create new `Set` and `Map` instances before changing them. The mounted `Inventory` component therefore shows the changed quantity, selection text, or list after each button click. The updater returns only the changed fields because Zustand's `set` operation merges the returned state at one level. A `Map` or `Set` is a nested value, so replacing that field with a new instance is the immutable update. For a delete, copy first, call `delete`, and return the copy. To clear a collection, return `new Map()` or `new Set()` for that field. ## Avoid in-place mutation Do not return the same collection reference after changing it: ```ts import { create } from 'zustand' const useInventoryStore = create<{ stock: Map mutateInPlace: () => void }>((set) => ({ stock: new Map([['apple', 12]]), mutateInPlace: () => set((state) => { state.stock.set('apple', 13) return { stock: state.stock } }), })) ``` That update mutates the existing `Map` and returns its existing reference. Zustand compares the next state with the current state using `Object.is`; a collection update must provide a new reference for the subscribed value to change. ## Type empty collections explicitly When a collection starts empty, give its element types in the store type or in the initializer. The store type in the example above does this. If you infer from an empty array directly, TypeScript can infer `never[]`, which prevents later additions. These initializers preserve the intended types: ```ts const ids = new Set([] as string[]) const users = new Map([] as [string, { name: string }][]) ``` ## See it running Open the [Map and Set demo](https://stackblitz.com/edit/vitejs-vite-5cu5ddvx) to see collection updates in a mounted app. ## Related - [Immutable state updates](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/immutable-state-updates) - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) - [Update nested state with Immer](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/update-nested-state-with-immer) # Test Zustand stores Use a DOM test environment for React components and use React Testing Library to exercise the rendered component. Jest and Vitest differ in their module-loading and test-runner configuration. ## When to use this pattern Use this setup when tests create stores with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) or [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore). Component tests interact with the rendered UI rather than calling implementation details. ## Share the store creator For the typed creator pattern, see [Typed store patterns](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/typed-store-patterns); this page applies that shared creator to test setup and assertions. ## Configure Jest Install the test dependencies, including `ts-jest` and `ts-node` for TypeScript configuration: ```bash npm install -D jest ts-jest ts-node jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event ``` Add the DOM matchers and configure Jest to use JSDOM: ```ts title="setup-jest.ts" import '@testing-library/jest-dom' ``` ```ts title="jest.config.ts" const config = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['./setup-jest.ts'], } export default config ``` The result is a Jest environment with a JSDOM document and Testing Library matchers. ## Configure Vitest Install the Vitest and DOM-testing dependencies: ```bash npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event ``` Enable JSDOM and the Testing Library matchers in the setup file: ```text title="setup-vitest.ts" import '@testing-library/jest-dom/vitest' ``` ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, environment: 'jsdom', setupFiles: ['./setup-vitest.ts'], }, }) ``` With globals disabled, import the Vitest globals used by your test files from `vitest`, and omit the `vitest/globals` type reference from `global.d.ts`. With globals enabled, add the Vitest type reference: ```ts title="global.d.ts" /// /// ``` ## Test a React component The component uses the hook returned by `create` and selects state and the action separately. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) is needed when a component consumes a standalone store; this component does not need it. ```tsx title="Counter.tsx" import { create } from 'zustand' import { counterStoreCreator } from './shared/counter-store-creator' export const useCounterStore = create<{ count: number inc: () => void }>()(counterStoreCreator) export function Counter() { const count = useCounterStore((state) => state.count) const inc = useCounterStore((state) => state.inc) return (
{count}
) } ``` Render the component, assert the initial UI, click the user-facing button, and assert the updated UI: ```tsx title="Counter.test.tsx" import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './Counter' test('increments the displayed count', async () => { const user = userEvent.setup() render() expect(screen.getByLabelText('count')).toHaveTextContent('1') await user.click(screen.getByRole('button', { name: 'Increment' })) expect(screen.getByLabelText('count')).toHaveTextContent('2') }) ``` The test sees `1` on the initial render and `2` after the click. ## Test a standalone store For the `StoreApi` methods, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works); this test uses `getState()` to invoke the action and inspect the resulting state. ```ts title="counter-store.ts" import { createStore } from 'zustand/vanilla' import { counterStoreCreator } from './shared/counter-store-creator' export const counterStore = createStore<{ count: number inc: () => void }>()(counterStoreCreator) ``` ```ts title="counter-store.test.ts" import { counterStore } from './counter-store' test('updates a standalone store through its action', () => { expect(counterStore.getState().count).toBe(1) counterStore.getState().inc() expect(counterStore.getState().count).toBe(2) }) ``` The test reads the state before and after the action. ## Use a vanilla store in a component For a component that receives or imports a standalone store, bind it with `useStore` and select only the value it renders. ```tsx title="VanillaCounter.tsx" import { useStore } from 'zustand' import { counterStore } from './counter-store' export function VanillaCounter() { const count = useStore(counterStore, (state) => state.count) const inc = useStore(counterStore, (state) => state.inc) return (
{count}
) } ``` The hook returns the selected value and subscribes the component to changes. Test it with the same rendered interaction: the component displays `1`, and the button changes it to `2`. ## Pitfalls - Use JSDOM for component tests. A test runner without a DOM cannot render React components. - For selector identity and shallow-rendering pitfalls, see [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). For server-rendered stores, see [Handle server-rendered stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/handle-server-rendered-stores). ## Related - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) - [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store) - [Reset store state](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/reset-store-state) - [React quick start](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/react-quick-start) # Start from the Zustand starter Use the React starter when you want a running example before adapting Zustand to your own application. It creates one store, selects state and an action in a component, and renders a counter that changes when you click `+1`. ## Run the starter The starter uses React 18, Vite, TypeScript, and Zustand. Run it from a clone of the Zustand repository: ```bash git clone https://github.com/pmndrs/zustand cd zustand pnpm install cd examples/starter pnpm install pnpm dev ``` Open the local address printed by Vite. You see the Zustand mascot, the heading `Zustand Starter`, the initial count `0`, and a `+1` button. Each click increments the displayed count. You can also open the starter in [StackBlitz](https://stackblitz.com/github/pmndrs/zustand/tree/main/examples/starter). Read the [starter source](https://github.com/pmndrs/zustand/blob/d7a5583cffd80af515f7dfb69583c95cbdc9e2ce/examples/starter) when you want to compare the running example with its files. ## Use the same structure in a small app This complete entry point keeps the store at module scope, selects the count and action separately, and mounts a working counter: ```tsx title="src/main.tsx" import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' type Store = { count: number inc: () => void } const useStore = create((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) function Counter() { const count = useStore((state) => state.count) const inc = useStore((state) => state.inc) return (

Count: {count}

) } const root = document.getElementById('root')! createRoot(root).render( , ) ``` The page initially shows `Count: 0` and a `+1` button. Clicking the button calls `inc`, and the rendered count increases. The starter's layout also depends on its global stylesheet. Keep this file when copying the example, or replace it with your application's layout styles: ```css title="examples/starter/src/index.css" html, body, #root { height: 100%; } #root { display: flex; place-items: center; justify-content: center; color: #fff; background-color: #131311; } ``` ## Adapt the structure 1. Install Zustand in your React project: ```bash npm install zustand ``` 2. Replace `Store` with the state and actions your feature owns. Keep actions beside the state they update. 3. Create the store at module scope with [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create), as the starter does. This gives components a stable hook to call. 4. In each component, select only the state or actions it renders or invokes. For example, `useStore((state) => state.count)` returns the selected number, while `useStore((state) => state.inc)` returns the action. 5. Render the selected state and pass the selected action to the relevant event handler. The mounted component then shows the current store value, and the action changes that value through `set`. The starter's complete result is a mounted React application with a store-backed counter. Adapt the `Store` type, the object passed to `create`, and the component markup while keeping this boundary between store logic and rendered UI. ## Related pages - [React quick start](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/react-quick-start) - [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store) - [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) - [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering) # 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-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/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-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand): `createStore`, `ExtractState`, `Mutate`, `StateCreator`, `StoreApi`, `StoreMutatorIdentifier`, `StoreMutators`