LEARN · DEBUGGING GUIDE

Zustand Persist Middleware Not Saving or Loading State

Zustand persist middleware can silently fail to save or load your state. Here’s how to spot the problem, get unstuck, and make persistence reliable.

IntermediateReact bugs4 min read

What this usually means

The expected persistence behavior—saving and rehydrating your Zustand store—is silently broken. This is usually a consequence of misconfiguration, serialization failures, incorrect storage setup, or subtle timing bugs where the persist middleware is set up incorrectly in relation to store creation. Debugging is tricky because Zustand’s persist middleware often fails quietly unless you check storage directly or add verbose logging.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check browser DevTools > Application > Storage for any keys matching your configured 'name'—if missing, persistence is not wiring up.
  • 2Insert console.log in the persist middleware’s onRehydrateStorage callback to confirm it actually runs on reload.
  • 3Wrap your store creation with persist() as shown in the docs, not inside create(), and double-check the import path for zustand/middleware.
  • 4Temporarily switch storage to sessionStorage to rule out localStorage quota or privacy issues.
  • 5Test in a private/incognito window for browser extension or privacy mode interference.
  • 6Inspect for circular references or unserializable values in your initial state. Use JSON.stringify to check.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchsrc/store.ts or wherever your Zustand store is defined
  • searchBrowser DevTools > Application > Local Storage
  • searchNetwork tab to ensure no CSP or cookie issues on reload
  • searchConsole logs for warnings about serialization or storage access
  • searchAny custom functions passed to the persist middleware (e.g. custom serialize/deserialize)
  • searchYour browser's indexedDB if you swapped to custom storage
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningPassing a non-serializable value (such as function, class instance, or circular reference) into initial state
  • warningConfiguring persist middleware inside the create() call instead of wrapping it
  • warningMismatched import paths (importing from 'zustand' instead of 'zustand/middleware') so persist() is undefined
  • warningStorage option set to undefined or to a custom object lacking getItem/setItem
  • warningBrowser storage quota exceeded (QuotaExceededError), often in private browsing
  • warningHydration callback missing or not handled properly, so rehydration silently fails
  • warningUsing persist on a store slice but not the root store in a modular setup
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways wrap create() with persist(), not the other way around: create = persist(create(fn), config)
  • buildExplicitly specify storage: storage: () => localStorage (not just storage: localStorage)
  • buildSanitize initial state with JSON.stringify to catch serialization problems up front
  • buildAdd error handling to the onRehydrateStorage callback for logging
  • buildSwitch to sessionStorage or a custom storage to debug storage-specific issues
  • buildPreemptively clear localStorage with localStorage.clear() during testing to avoid old/corrupt state
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedReload the page and confirm state persists and matches prior values without initialization fallback
  • verifiedInspect storage directly for expected key and up-to-date serialized payload
  • verifiedAdd a test that renders, updates state, reloads, and verifies state is still updated
  • verifiedCheck that the onRehydrateStorage callback fires on each page load
  • verifiedDeliberately break state structure (e.g., add a function to state) and confirm persist throws/logs expected error
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAssuming persist works without looking at storage—always check DevTools
  • warningIgnoring console warnings about serialization failures
  • warningConfiguring persist after create() instead of as a wrapper
  • warningLeaving default storage (which may be undefined in Node/test environments)
  • warningPersisting non-root slices in a modular store setup: persist the complete store
  • warningNot handling/monitoring rehydration lifecycle (e.g., not using onRehydrateStorage)
( 07 )War story

Silent Loss of Persisted State in Zustand Store

Frontend EngineerReact 18.2, Zustand 4.4.1, zustand/middleware, Chrome 114

Timeline

  1. 10:01Deployed new dashboard with Zustand and persist middleware enabled.
  2. 10:10Users report their settings reset after every page reload.
  3. 10:12Checked localStorage in DevTools—no keys matching store name.
  4. 10:13Noticed persist middleware was used as create(persist(...)) instead of persist(create(...)).
  5. 10:15Corrected middleware order and redeployed.
  6. 10:18State now persisted, but rehydration was inconsistent in incognito mode.
  7. 10:20Found initial state included a function, breaking serialization in private windows.
  8. 10:23Sanitized state and confirmed persistence now reliable across all browsers.

Shortly after deploying a feature that saved user dashboard layout via Zustand, users complained their preferences vanished on reload.

I initially thought this was a browser privacy setting, but poking into localStorage showed no state ever being written. I realized our store was created incorrectly: persist middleware was passed into create(), not as a wrapper around it.

After fixing that, persistence worked—except in incognito or Safari private mode, where state sometimes still failed to rehydrate. It turned out our store held a function as part of state, which JSON.stringify choked on and triggered silent failure. A final cleanup solved it for good.

Root cause

Incorrect persist middleware usage and non-serializable state values (function in state).

The fix

Wrap create() with persist() properly and ensure all initial state is serializable.

The lesson

Zustand persist middleware will fail silently—validate configuration, always check storage writes, and log rehydration.

( 08 )How persist Middleware Actually Wraps Zustand Stores

The persist() enhancer needs to be the outermost wrapper around create(), not used inside or after the store is created. For example, use create = persist(create(fn), { ... })—not create(persist(fn, { ... })).

If the order is wrong, no state will ever be written to or read from storage, and you often get zero feedback in the browser or logs.

( 09 )Serialization Pitfalls: Functions, Classes, and Circular References

Zustand persists state as JSON by default using JSON.stringify. Any non-serializable value—functions, DOM elements, classes, circular data—will cause complete failure to save state, usually with no visible error.

Sanitize your state with JSON.stringify(state) before passing it to persist, or use custom (de)serializers for complex objects.

( 10 )Storage Edge Cases: Quota, Incognito, and Permissions

LocalStorage can fail or be unavailable in iOS Safari private mode, Chrome incognito, or if browser quota is exceeded. When this happens, Zustand catches the error and persistence fails silently.

Test in private/incognito modes and handle the storage errors in onRehydrateStorage for better debuggability.

( 11 )Diagnosing Persisted State in the Browser

Open DevTools > Application > Local Storage and look for a key matching the persist config’s name. If absent, persistence is not working.

You can edit, clear, or inspect these keys directly to confirm what’s actually being written and read on every reload.

Frequently asked questions

Can I use persist middleware on a Zustand slice instead of the root store?

No. Persist must wrap the root store creation. If you use it on a slice, only that slice is saved (if at all), and rehydration can break silently.

Why does my state persist in development but fail in production?

You may have a serialization error or the persist wrapper order is wrong, but your development build hides it. Check for minified code errors, or CSP and storage quota differences in prod.

How do I debug onRehydrateStorage failures?

Add a console.log or error logger in the onRehydrateStorage callback to catch initialization, any thrown errors, and confirm it runs on every page load.

Is it possible to use custom storage engines with Zustand persist?

Yes, but your custom storage must implement getItem, setItem, and removeItem, and handle errors gracefully. Test it directly before integrating.