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

  1. 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.
  2. 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`.
  3. Gate the mutation explicitly: wrap the call in `if (typeof window !== 'undefined') { ... }` or pass `isSSR={false}` only when you have confirmed a client context.
  4. 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.
  5. 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.
  6. 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

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.