pmndrs/zustand · error · Error
Cannot set state of Zustand store in SSR
Error message
Cannot set state of Zustand store in SSR
What it means
This error is thrown by Zustand's experimental `ssrSafe` middleware (exported as `unstable_ssrSafe`). The middleware detects server-side rendering (via `typeof window === 'undefined'` or the explicit `isSSR` arg) and, when true, replaces the store's `set` function and `api.setState` with a stub that throws. Its purpose is to fail fast: any state mutation during SSR would produce a hydration mismatch or pollute shared server state, so the middleware surfaces the offending call site immediately instead of letting it silently corrupt the render.
Source
Thrown at src/middleware/ssrSafe.ts:20
// This is experimental middleware. It will be changed before finalizing it.
// https://github.com/pmndrs/zustand/discussions/2740
// TODO Not very happy with the middleware name. Will revisit it later.
export function ssrSafe<
T extends object,
U extends object,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
>(
config: StateCreator<T, Mps, Mcs, U>,
isSSR: boolean = typeof window === 'undefined',
): StateCreator<T, Mps, Mcs, U> {
return (set, get, api) => {
if (!isSSR) {
return config(set, get, api)
}
const ssrSet = () => {
throw new Error('Cannot set state of Zustand store in SSR')
}
api.setState = ssrSet
return config(ssrSet as never, get, api)
}
}
View on GitHub (pinned to beca84e600)
Solutions
- Move the `set(...)` call out of the server code path: put it inside `useEffect` (effects do not run during SSR) or inside an event handler / user action.
- Stop mutating the store to pass server-fetched data; instead return the data from `getServerSideProps`/loader as props and seed the store via its initial state in the creator (`create(initialState)`), not via a post-creation `setState`.
- Gate the mutation explicitly: wrap the call in `if (typeof window !== 'undefined') { ... }` or pass `isSSR={false}` only when you have confirmed a client context.
- If the mutation is a legitimate one-time initialization, perform it in the store creator's body before returning state, or use `createJSONStorage`/hydration APIs rather than calling `set` after creation.
- Search the stack trace for the first `set`/`setState` call and trace it back to its caller; the offending caller is the one running during SSR — relocate it.
- If SSR mutation is genuinely desired (e.g. seeding per-request state), do not use `ssrSafe` for that store; use a per-request store instance or `createStore` scoped to the request instead.
Example fix
// before — action runs during SSR (e.g. called from getServerSideProps)
export const useUser = create<UserState>()(
unstable_ssrSafe((set) => ({
user: null,
loadUser: async (id) => {
const u = await fetchUser(id)
set({ user: u }) // throws: 'Cannot set state of Zustand store in SSR'
},
})),
)
// after — seed from server props, fetch only on the client
export const useUser = create<UserState>()(
unstable_ssrSafe((set, get) => ({
user: null,
hydrate: (u) => set({ user: u }), // called from a Client Component with SSR props
loadUser: async (id) => {
if (typeof window === 'undefined') return
const u = await fetchUser(id)
set({ user: u })
},
})),
)
// page: getServerSideProps returns { props: { initialUser } },
// and a <ClientUser initialUser={initialUser} /> calls hydrate(initialUser) in useEffect. Defensive patterns
Strategy: validation
Validate before calling
// Run before calling any store action that may mutate.
// Returns true when it is safe to call set/setState under ssrSafe.
function canMutateStore(isSSRFlag = typeof window === 'undefined'): boolean {
return !isSSRFlag
}
// usage
if (canMutateStore()) {
useUser.getState().loadUser(id)
} Type guard
// Narrowing helper: only call the mutating branch inside the guarded callback.
function isClient(): boolean {
return typeof window !== 'undefined'
}
// usage in a component
useEffect(() => {
if (!isClient()) return
loadUser(id)
}, [id]) Try / catch
// Not recommended: this error signals a real SSR design bug, so catching it
// hides the symptom. Prefer validation (gate with typeof window). If you must
// isolate a third-party call, catch narrowly and rethrow non-SSR errors.
try {
store.setState({ user })
} catch (err) {
if (err instanceof Error && /SSR/.test(err.message)) {
// expected during SSR; defer to client
return
}
throw err
} Prevention
- Never call store setters from `getServerSideProps`, `getStaticProps`, route handlers, or React Server Components — pass data as props and hydrate on the client.
- Default every mutation to live inside `useEffect` or an event handler; only the store creator should set initial state at construction time.
- Keep an `isClient()` helper (`typeof window !== 'undefined'`) and use it as the single source of truth for client-only branches.
- Per-request SSR data belongs in a scoped store instance or in framework state/props, never in a module singleton mutated on the server.
- When adopting `unstable_ssrSafe`, run a server-only smoke test (e.g. render the page to string in Node) so any SSR mutation fails in CI, not in production.
- Pass the `isSSR` argument explicitly instead of relying on the default `typeof window` check when your framework customizes the SSR signal.
When it happens
Trigger: A store created with `create<T>()(unstable_ssrSafe(...))` whose `set()` or `api.setState()` is invoked while `typeof window === 'undefined'`. Concretely: (a) a store action calling `set(...)` is invoked from Next.js `getServerSideProps`/`getInitialProps`/a Route Handler/React Server Component; (b) the store initializer (or an auto-fetch it triggers) runs a `set` at module load on the server; (c) a callback passed into server-rendered JSX calls an action that mutates during render; (d) any code path that calls `useStore.setState(...)` or the `set` arg outside of `useEffect`/event handlers while on the server.
Common situations: Migrating a CRA/Vite app to Next.js/Remix and forgetting that store actions now run on the server; an RTK/zustand-style 'fetch on init' pattern that calls `set` inside the creator; React 18 server components calling client store setters; a shared singleton store mutated during `getServerSideProps` to pass data to the page (should be props instead); toggling `isSSR` incorrectly or relying on a stale `typeof window` check after a framework upgrade that changed SSR timing; effects that are not actually effect-gated and run during server render in strict mode.
AI-assisted analysis of pmndrs/zustand@beca84e600 (2026-08-12).
Data as JSON: /api/errors/9e2da92f4da58e2c.
Report an issue: GitHub.