Zustand Persist
Persist Zustand store to localStorage or custom storage with automatic rehydration and migration support
When to Use
- Preserving user preferences, theme settings, or cart contents across page reloads
- Building offline-capable features that need state survival
- Selectively persisting some state fields while keeping others ephemeral
- Migrating persisted state when the store shape changes between versions
Instructions
- Wrap the store creator with the
persistmiddleware fromzustand/middleware. - Provide a
name— this is the localStorage key. Make it unique per store. - Use
partializeto persist only specific fields. By default, the entire store is persisted. - Use
storageto change the storage backend (sessionStorage, IndexedDB, AsyncStorage). - Use
versionandmigrateto handle schema changes between app versions. - Handle the rehydration delay — state is synchronously initialized with defaults, then asynchronously rehydrated from storage.
// stores/settings-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface SettingsStore {
theme: 'light' | 'dark';
locale: string;
sidebarOpen: boolean;
setTheme: (theme: 'light' | 'dark') => void;
setLocale: (locale: string) => void;
toggleSidebar: () => void;
}
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'light',
locale: 'en',
sidebarOpen: true,
setTheme: (theme) => set({ theme }),
setLocale: (locale) => set({ locale }),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}),
{
name: 'app-settings',
// Only persist theme and locale — not sidebarOpen
partialize: (state) => ({
theme: state.theme,
locale: state.locale,
}),
}
)
);
Details
Rehydration timing: On first render, the store uses the default values defined in create. The persisted values load asynchronously from storage. Use onRehydrateStorage to detect when rehydration completes:
persist(storeCreator, {
name: 'app-settings',
onRehydrateStorage: () => {
return (state, error) => {
if (error) console.error('Rehydration failed', error);
else console.log('Rehydrated', state);
};
},
});
Waiting for rehydration in components:
// Option 1: useStore.persist.hasHydrated()
function App() {
const hasHydrated = useSettingsStore.persist.hasHydrated();
if (!hasHydrated) return <Spinner />;
return <Main />;
}
// Option 2: onFinishHydration listener
useEffect(() => {
const unsub = useSettingsStore.persist.onFinishHydration(() => {
setReady(true);
});
return unsub;
}, []);
Custom storage: For non-localStorage backends:
const indexedDBStorage = createJSONStorage(() => ({
getItem: async (name) => {
/* read from IndexedDB */
},
setItem: async (name, value) => {
/* write to IndexedDB */
},
removeItem: async (name) => {
/* delete from IndexedDB */
},
}));
persist(storeCreator, { name: 'key', storage: indexedDBStorage });
Migrations:
persist(storeCreator, {
name: 'app-settings',
version: 2,
migrate: (persistedState, version) => {
const state = persistedState as any;
if (version < 2) {
// v1 had 'darkMode: boolean', v2 uses 'theme: string'
state.theme = state.darkMode ? 'dark' : 'light';
delete state.darkMode;
}
return state;
},
});
What NOT to persist: Loading states, error messages, transient UI state, data that should be fetched fresh from the server.
Source
https://zustand.docs.pmnd.rs/middlewares/persist
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.