Persisting Zustand State with the persist Middleware: Setup, Selective Sync, and SSR Gotchas
Learn how to configure Zustand's persist middleware to automatically save and restore store state in the browser, filter which parts of state are persisted, and avoid common hydration mismatches in server‑side rendered apps.
23 Nov 2025, 07:51 UTC

The problem: losing UI state on refresh
When you build a React app with Zustand, any state kept only in memory disappears as soon as the user reloads the page or navigates away. This forces you to re‑fetch data or reset UI controls, which can feel jarring and waste bandwidth.
Thesis: Zustand’s persist middleware can automatically sync store slices with browser storage, letting you keep state across sessions while retaining control over what gets saved.
Enabling persist
First install Zustand if you haven’t already. Run this in your project’s root directory:
# terminal, no special permissions required
npm install zustand
Then create a store and wrap it with the persist middleware. The middleware accepts an options object where you define the storage key and the storage engine (default is localStorage).
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
count: 0,
theme: 'light',
increment: () => set((state) => ({ count: state.count + 1 })),
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
}),
{
name: 'my-app-storage', // key in localStorage
// storage: sessionStorage // optional: swap engine
}
)
)
export default useStore
After the store is created, any call to increment or toggleTheme updates the state and, thanks to the middleware, writes the whole state object as a JSON string to localStorage under the key my-app-storage. On the next page load, Zustand reads that string and hydrates the store automatically.
Selective persistence with partialize
Sometimes you only want to save a slice of the store (e.g., UI preferences) while keeping derived or volatile data in memory. The partialize option lets you return a subset of the state for serialization.
const useStore = create(
persist(
(set) => ({
count: 0,
theme: 'light',
// derived value, not persisted
doubleCount: 0,
increment: () => set((state) => ({
count: state.count + 1,
doubleCount: (state.count + 1) * 2
})),
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
}),
{
name: 'my-app-storage',
partialize: (state) => ({
// only persist theme; count is kept in memory if you prefer
theme: state.theme
})
}
)
)
In this example, only theme is written to storage; count and doubleCount reset on reload. You can adjust the return object to persist any combination of fields.
SSR considerations and trade‑offs
In a server‑side rendered environment (e.g., Next.js), the server cannot access localStorage. If you attempt to read the persisted value during the initial render, the server and client may end up with different state—a hydration mismatch. A common mitigation is to defer reading the persisted value until the client mounts, or to provide a fallback initial state that matches the server’s render.
Another limitation is storage size. Browsers typically cap localStorage at around 5 MB. Storing large objects (e.g., nested arrays of API responses) can cause slowdowns or exceed the quota, leading to silent failures. Keep persisted data limited to lightweight UI settings or small caches.
Verifying the setup
- Run your app and open the browser’s developer tools.
- Navigate to the Application tab → Local Storage → your origin.
- Look for the key you set (
my-app-storage) and confirm its value is a JSON string containing the persisted fields. - Refresh the page and check that the store reflects those values (e.g., theme stays dark if you toggled it).
If you see the key missing or the value malformed, verify that the middleware is wrapped around the store creator and that the name option matches what you inspect.
Actionable closing
To add persistence to a Zustand store:
- Install
zustandif needed. - Wrap your store creator with
persistand provide aname(storage key). - Optionally supply
partializeto limit what gets saved. - In SSR frameworks, guard the store usage with a client‑only check or provide a matching initial state.
- Confirm persistence via the devtools Application tab.
By following these steps you keep essential UI state across reloads without writing custom storage logic, while staying aware of size limits and hydration pitfalls.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.