Persisting Zustand Store State with the persist Middleware
Learn how Zustand's persist middleware automatically saves and restores store state in localStorage, with a worked example, trade‑offs, and verification steps.
23 Mar 2026, 06:51 UTC

Problem: UI state disappears on refresh
When you build a React app with Zustand, any state kept in a store is lost as soon as the page reloads. Manually saving and restoring values to localStorage or sessionStorage adds boilerplate and is easy to forget.
Thesis: Zustand’s persist middleware automates hydration and synchronization
The persist middleware wraps a store creator, reads a stored value (if any) to initialize the store, and writes every state change synchronously to the chosen storage API. This gives you automatic persistence with minimal code.
How persist works under the hood
When you call create(persist(...)), the middleware:
- Looks for a value in the storage identified by the
nameoption. - If found, it parses the JSON and uses it to set the initial store state.
- After that, each state update triggers a synchronous
setItemcall, keeping storage in sync with memory.
Because it only relies on the getItem, setItem, and removeItem methods, you can swap the default localStorage for sessionStorage, a custom async wrapper, or an IndexedDB‑based adapter without changing the store logic.
Configuration options
name(string): the storage key; required.storage(object): defaults tolocalStorage. Provide any object with the three storage methods.version(number): enables schema migration; when the stored version differs, themigratefunction runs.migrate(function): receives the persisted state and the stored version, returns the migrated state.
Worked example: a counter that survives reloads
// 1. Install Zustand (run in your project root)
// npm i zustand
// 2. Create a persisted store
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useCounter = create(
persist(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 })
}),
{
name: 'counter-storage', // key in localStorage
// storage: sessionStorage, // uncomment to use sessionStorage instead
}
)
)
// 3. Use the store in a React component
import React from 'react'
def Counter() {
const { count, inc, reset } = useCounter()
return (
Count: {count}
+1
Reset
)
}
export default Counter
After clicking the +1 button and refreshing the page, the counter shows the last value because the middleware wrote {count: N} to localStorage under the key counter-storage.
Trade‑offs and limitations
- Serializability: Only JSON‑serializable values can be persisted. Storing a function, class instance, or circular object will cause an error during
setItemor produceundefinedon rehydration. - Synchronous I/O: Each state update triggers a blocking write. For large stores (e.g., >100 KB) this can cause noticeable UI jank. Consider debouncing writes or using an async storage adapter if payloads grow.
- Storage key collisions: If two stores share the same
name, they will overwrite each other’s data. Choose unique keys.
How to verify persistence works
- Run the app, interact with the store (e.g., increment the counter).
- Open Chrome DevTools → Application tab → Local Storage →
http://localhost:3000(or your origin). - Locate the key
counter-storage. Its value should be a JSON string matching the current store state. - Refresh the page; the component should render the same value you saw before the reload.
- To test the serialization limit, try storing a non‑serializable value (e.g.,
fn: () => {}) and observe the error in the console.
Actionable next steps
Add Zustand to your project, wrap any store that needs to survive reloads with persist, pick a unique name, and verify the entry in DevTools. If you notice UI lag with large state, migrate to an async storage adapter or debounce writes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.