Zustand useSyncStorage: When Synchronous Persistence Makes Sense
For small apps needing simple persistence, useSyncStorage provides synchronous localStorage sync with minimal setup. Here's when it works and when it doesn't.
13 Nov 2025, 04:38 UTC

The Persistence Problem
You need a React state management solution that survives page reloads without writing boilerplate rehydration code. The challenge is choosing between async persistence (good for large state) and sync persistence (simple, immediate). For small to medium apps where the persisted state fits comfortably in localStorage, useSyncStorage offers a pragmatic middle ground.
Requirements
- State must persist across page refreshes
- Initial load must rehydrate from storage without flicker
- Implementation should be minimal—no custom event listeners
- Storage key must be stable; changing it invalidates previous data
- Persisted state should be small enough to avoid quota errors
The Smallest Suitable Design
useSyncStorage wraps a slice of your zustand store and keeps it synchronized with localStorage. It reads on mount, writes on every change, and handles JSON serialization automatically.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({ count: (state) => state + 1 }),
{ name: 'my-app-state' }
)
);
export default useStore;
This uses the built-in persist middleware, which internally relies on useSyncStorage for v4+. The name option becomes the localStorage key.
Trust and Data Boundaries
localStorage is not secure. Never store sensitive data like tokens or user credentials. The middleware serializes any JSON-serializable value, so circular references or functions will throw.
Operational Checks
- Verify storage key exists: Open DevTools, go to Application → Storage → Local Storage, confirm
my-app-stateappears. - Test rehydration: Modify state in the app, hit refresh, ensure the value updates in DevTools.
- Check quota: If state exceeds ~5MB, localStorage may silently fail. Monitor
navigator.storage.estimate()for remaining quota.
Failure Modes
- Quota exceeded: Writes fail silently; catch errors by wrapping
localStorage.setItemin a try-catch during debugging. - Key mutation: Changing the storage key after initial load means old data stays under the previous key; new data starts empty.
- Heavy state: Large objects cause UI jank on write because the operation is synchronous and blocks the main thread.
When This Design Breaks
If your state grows beyond a few hundred kilobytes, consider async persistence with persist middleware configured to use createJSONStorage with an async storage adapter, or move to IndexedDB-backed solutions like zustand/middleware with custom storage.
Conclusion
useSyncStorage is ideal for small apps requiring simple, immediate persistence. It eliminates rehydration code and works out of the box with zustand v4+. Monitor state size and keep sensitive data out of localStorage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.