# How to Handle SSR Safety with the ssrSafe Middleware in Zustand

> Safely manage SSR state in Zustand with unstable_ssrSafe middleware. Prevent server-side mutations and avoid hydration mismatches. Learn how to implement it.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**The `unstable_ssrSafe` middleware prevents server-side state mutations by replacing `setState` with a function that throws, eliminating hydration mismatches between server and client renders.**

Zustand provides an experimental SSR-safety middleware to enforce immutable stores during server-side rendering. By wrapping your store creator with `ssrSafe`, you ensure that state initialization can occur on the server while mutations are strictly blocked until hydration completes. This implementation, found in the pmndrs/zustand repository, guarantees deterministic renders and surfaces accidental state updates as explicit runtime errors.

## What the ssrSafe Middleware Does

The middleware intercepts store creation and detects the runtime environment using **`typeof window === 'undefined'`** (or a custom flag you provide). When executing in a server context, it wraps the store API to prevent mutations.

In [[`src/middleware/ssrSafe.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/ssrSafe.ts)](https://github.com/pmndrs/zustand/blob/main/src/middleware/ssrSafe.ts), the implementation follows this pattern:

```typescript
export function ssrSafe<T extends object, U extends object, ...>(
  config: StateCreator<T, ..., U>,
  isSSR: boolean = typeof window === 'undefined',
): StateCreator<T, ..., U> {
  return (set, get, api) => {
    if (!isSSR) {
      // Normal client execution proceeds unchanged
      return config(set, get, api)
    }
    // Server environment: replace set with a function that throws
    const ssrSet = () => {
      throw new Error('Cannot set state of Zustand store in SSR')
    }
    api.setState = ssrSet                // Block any later mutation attempts
    return config(ssrSet as never, get, api) // Initialize with safe setter
  }
}

```

Key behaviors of this approach:

- **Fast failure**: Any call to `set()` or `api.setState` during SSR throws immediately with the message *"Cannot set state of Zustand store in SSR"*.
- **Initialization preserved**: The original store configuration function still executes, allowing default state values to be established for the initial HTML snapshot.
- **Global protection**: By overwriting `api.setState` directly, the middleware blocks mutations even from external code holding a reference to the API object.

The middleware is re-exported from [[`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts)](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts) under the **unstable** namespace:

```typescript
export { ssrSafe as unstable_ssrSafe } from './middleware/ssrSafe.ts'

```

## Why You Need SSR Protection

Without the `ssrSafe` middleware, Zustand stores created on the server can accidentally mutate during the render phase. When a component updates global state inside `useEffect` or during initial data fetching, the server-rendered HTML may diverge from the client's first render, causing **hydration mismatches** or silent state corruption.

By applying `ssrSafe`, you achieve:

- **Deterministic server renders**: The HTML snapshot remains identical across every request because state cannot change during the React render cycle.
- **Early error detection**: Accidental server-side state updates surface as explicit exceptions during development rather than subtle hydration bugs in production.
- **Client-side interactivity**: Once the browser hydrates the application, the store automatically accepts mutations and behaves normally.

## How to Apply the Middleware

Import `unstable_ssrSafe` from the main middleware entry point and wrap your store creator function:

```typescript
import { create } from 'zustand'
import { unstable_ssrSafe as ssrSafe } from 'zustand/middleware'

// SSR-protected store
const useStore = create(
  ssrSafe((set) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
    dec: () => set((state) => ({ count: state.count - 1 })),
  }))
)

```

The middleware accepts an optional second argument for custom SSR detection logic. This is useful for frameworks like Next.js that expose specific environment flags:

```typescript
const isServer = () => 
  typeof window === 'undefined' || process.env.NEXT_RUNTIME === 'nodejs'

const useEdgeStore = create(
  ssrSafe(
    (set) => ({
      data: null,
      load: (payload) => set({ data: payload }),
    }),
    isServer() // Custom detection boolean
  )
)

```

## React 18 Hydration Integration

When using React 18's `renderToString` and `hydrateRoot`, the `ssrSafe` middleware ensures the store remains immutable during the server pass but reactive after hydration:

```tsx
import React, { useEffect } from 'react'
import { create } from 'zustand'
import { unstable_ssrSafe as ssrSafe } from 'zustand/middleware'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'

const useCounter = create(
  ssrSafe((set) => ({
    value: 0,
    inc: () => set((s) => ({ value: s.value + 1 })),
  }))
)

function Counter() {
  const { value, inc } = useCounter()

  // Effects do not run during SSR, so inc() only executes client-side
  useEffect(() => {
    inc()
  }, [inc])

  return <div>Count: {value}</div>
}

// Server render: store is initialized but immutable
const html = renderToString(<Counter />)

// Client hydration: store becomes mutable and interactive
hydrateRoot(document.getElementById('root')!, <Counter />)

```

During the server render, any accidental call to `setState` (for example, inside a synchronous data fetcher) throws an error immediately. On the client, the store initializes with the server-provided state and accepts updates normally.

## Summary

- **Import path**: Access `unstable_ssrSafe` from `zustand/middleware` (re-exported from [`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts)).
- **Implementation location**: Core logic resides in [`src/middleware/ssrSafe.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/ssrSafe.ts).
- **Detection strategy**: Defaults to `typeof window === 'undefined'` but accepts custom boolean flags.
- **Protection mechanism**: Replaces `api.setState` with a function that throws on the server while allowing state initialization to proceed.
- **Use case**: Prevents hydration mismatches in Next.js, Remix, or custom SSR environments by enforcing immutable stores during server rendering.

## Frequently Asked Questions

### Does ssrSafe prevent reading state on the server?

No. The middleware only blocks mutations via the `set` function and `api.setState`. Reading state through `get` or accessing the store's current values remains fully functional during server-side rendering, allowing components to initialize with default data for the HTML snapshot.

### Can I use ssrSafe with the Next.js App Router?

Yes. While the App Router handles server components differently, you can use `ssrSafe` in client components ("use client") that need protection during the initial server render pass or in streaming SSR scenarios. Pass a custom `isSSR` detector if you need to account for specific Next.js runtime flags like `process.env.NEXT_RUNTIME`.

### What happens if I call setState during SSR?

The middleware throws an immediate error with the message **"Cannot set state of Zustand store in SSR"**. This prevents the mutation from completing and alerts you to the problematic code location during development, allowing you to move the state update to a client-only lifecycle method like `useEffect`.

### Is the ssrSafe middleware production-ready?

The middleware is currently marked as **experimental** and exported under the `unstable_` prefix. While functional, the API may change before a stable release. For production applications, pin your Zustand version and monitor the repository for updates regarding the stabilization of this feature.