Technology handbook
Zustand
Stores, selectors, actions, middleware, persistence, TypeScript, SSR and testing.
- Topics
- 34
- Sections
- 5
Hooks
useStore with a selector
Call the store hook with a selector so only that slice's changes re-render the component.
- Select a primitive or stable reference from the store.
- Unrelated state updates should not change the selected value.
- Keep selector computation free of side effects.
useStoreWithEqualityFn
Supply a custom equality function where shallow comparison is not the right test.
- Import the equality-aware hook from zustand/traditional.
- Pass the vanilla store, selector and comparison function.
- Ensure equality checks all fields the component actually uses.
createWithEqualityFn
Create a store whose hook accepts an equality function at each call site.
- Use the traditional entry point for equality-aware bound hooks.
- Choose a default comparison appropriate to the selected data.
- Account for its use-sync-external-store peer dependency.
Binding a vanilla store
Read a framework-independent store from React without creating a second source of truth.
- Create the vanilla store once for its intended scope.
- Pass that instance to React's Zustand useStore hook.
- Reuse its getState and actions outside React.
Context-scoped store hooks
Create the store per provider and expose it through one typed wrapper hook.
- Keep a stable store instance inside the provider.
- Throw a clear error when the wrapper lacks its provider.
- Initialize each provider from its own input data.
Named slice hooks
Export a hook per slice so no call site can select the whole store by accident.
- Expose focused hooks such as useCartCount or useCartItems.
- Keep action selection separate from frequently changing values.
- Let callers depend on a stable domain-facing API.
Hook-free access
getState and subscribe cover event handlers and non-React code, where no hook may run.
- Use getState for a current imperative snapshot.
- Use subscribe when outside code needs ongoing notifications.
- Call the returned unsubscribe function during teardown.
Hooks in tests
Reset the store between tests and read state through the same hook the component uses.
- Render hook consumers with the correct scoped store.
- Wrap direct store updates in React act when needed.
- Restore initial state so tests cannot inherit earlier changes.
Store fundamentals
create and bound hooks
Create a React-bound store and call the returned hook with a selector inside a component.
- The create function returns a hook with store utilities attached.
- Define actions alongside initial state in the initializer.
- Invoke the hook only where React hooks are allowed.
createStore and vanilla stores
Use a framework-independent store with getState, setState and subscribe.
- createStore returns a StoreApi without binding it to React.
- Read snapshots with getState and update through actions.
- Attach subscriptions only for their required lifetime.
State and actions
Keep update rules in named actions so callers express intent instead of editing store internals.
- Name actions after domain operations such as addItem.
- Keep related field updates together inside one action.
- Expose only the state consumers need to read.
Functional updates
Read the latest previous state when computing a new value from the old one.
- Use set with a callback when the next value depends on state.
- Read previous values from that callback's state argument.
- Avoid captured render snapshots for sequential updates.
Shallow merging and replacement
Understand the difference between merging fields and replacing the entire state, including actions.
- Default set merges fields at the top level only.
- Nested objects still require deliberate copying or replacement.
- Replacement mode also removes omitted action functions.
Immutable nested updates
Copy changed nested objects or use a suitable Immer integration to notify subscribers correctly.
- Copy each changed branch before assigning the new root field.
- Retain references for nested data that did not change.
- Do not mutate arrays obtained from getState.
Selectors & subscriptions
Atomic selectors
Subscribe to the smallest value needed by a consumer instead of the entire store.
- Select a single field when only that field is displayed.
- Prefer separate subscriptions for independent primitives.
- Avoid selecting a whole collection for one item's count.
Stable selector results
Avoid returning a fresh object on every snapshot read when the values have not changed.
- A new object or array can look changed on every read.
- Reuse derived references when their inputs are unchanged.
- Prefer primitives or shallow stabilization for simple selections.
useShallow
Reuse an appropriate object or array selection when shallow equality captures its semantics.
- Wrap a selector with useShallow for shallow-equal outputs.
- Use it for suitable flat arrays or object selections.
- Nested mutation cannot be repaired by shallow comparison.
subscribeWithSelector
Observe selected state outside React and clean up subscriptions when their owner is removed.
- Enable selector-aware subscribe through the middleware.
- Provide a selector and listener for the external consumer.
- Unsubscribe when the listener's owning feature is disposed.
Transient updates
Drive an external renderer from a subscription without forcing a React render for every update.
- Subscribe directly for rapid canvas or animation updates.
- Update the external target without storing duplicate React state.
- Clean up the subscription when the target unmounts.
Derived state
Compute values from canonical state rather than storing redundant copies that can diverge.
- Derive totals and filters from canonical store fields.
- Memoize expensive derivations when input identities are stable.
- Do not synchronize a second stored copy unnecessarily.
Middleware & architecture
persist middleware
Persist selected state with a deliberate storage key, version and hydration strategy.
- Choose a stable storage name and explicit storage adapter.
- Persist durable user preferences rather than temporary requests.
- Decide how the UI behaves before rehydration completes.
partialize and migrations
Store only durable fields and transform older saved state when its shape changes.
- Use partialize to exclude actions and transient fields.
- Increment the persistence version when saved shape changes.
- Test migration against real older stored payloads.
devtools middleware
Name actions and inspect transitions while excluding sensitive application data.
- Give the store and actions meaningful debugging names.
- Inspect before-and-after state for unexpected transitions.
- Disable or sanitize sensitive data in shared diagnostics.
Immer middleware
Use draft-based updates within the middleware's contract and understand reference changes.
- Draft mutation syntax is valid inside Immer-managed updates.
- Return a replacement or mutate the draft, not both.
- Use draftable structures and test nested reference changes.
Slices pattern
Compose domain slices into a store with explicit ownership and compatible middleware typing.
- Define one slice per clearly owned domain.
- Combine slice creators into the final store initializer.
- Apply store-wide middleware at the combined boundary.
Context-scoped stores
Create separate store instances for independently mounted features or request-specific initial state.
- Use independent instances for separate editors or feature mounts.
- Supply the intended instance through React context.
- Dispose subscriptions when that scope is removed.
Async actions and races
Represent pending and failed operations and prevent stale responses from overwriting newer state.
- Store loading and error state with each asynchronous operation.
- Compare request identities before accepting late results.
- Abort obsolete requests when the underlying API supports it.
Resetting state
Restore initial data while preserving action functions and clearing persisted state when required.
- Use the store's initial snapshot to restore defaults.
- Preserve action functions when resetting data fields.
- Clear persistent storage when reset means forgetting saved data.
Zustand integration
TypeScript store types
Type state, actions and middleware composition without unsafe casts hiding incompatible updates.
- Declare separate state and action types for the store.
- Preserve middleware type information through composition.
- Infer selectors from the actual store state type.
Next.js and SSR
Scope stores correctly and ensure initial server and client snapshots agree.
- Create per-request stores for server-derived user data.
- Use matching initial snapshots during server render and hydration.
- Access browser-only storage after the client is ready.
Hydration and persistence
Avoid displaying persisted browser data as though it existed in the initial server render.
- Represent hydration completion explicitly for persisted views.
- Avoid reading localStorage during server rendering.
- Decide how persisted values merge with fresh server state.
Testing stores
Reset store state between tests and verify actions and selector-visible outcomes.
- Test actions against representative initial and edge-case states.
- Assert selected values and unchanged unrelated references.
- Include persistence migration and race-condition scenarios.
Zustand versus server-state caches
Keep remote-data freshness and retry policies in a tool designed for server state when appropriate.
- Use a server-state cache for freshness, retries and deduplication.
- Keep local interaction state in the Zustand store.
- Avoid maintaining a competing copy of query data.
Zustand versus Redux
Compare update discipline, middleware needs and team debugging requirements before choosing a store.
- Compare lightweight store actions with Redux's event pipeline.
- Consider auditability, middleware and team conventions.
- Choose based on actual sharing and debugging requirements.
Sources
- Zustand official documentation