From harness-claude
Persist Zustand stores to localStorage or custom storage with rehydration and migration support. Use for preserving preferences, themes, carts across reloads or offline features.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Persist Zustand store to localStorage or custom storage with automatic rehydration and migration support
Implements Zustand middleware for persistence (persist), devtools integration, Immer immutability, and custom store enhancements. Guides composition, best practices, partialize, and migrations.
Implements Zustand state management for React with TypeScript: global state, Redux/Context migration, localStorage persistence, slices pattern, devtools, Next.js SSR, hydration errors, infinite re-renders.
Creates lightweight global stores with Zustand's create function for minimal-boilerplate React state management. Use for auth, UI flags, preferences without prop drilling or Redux complexity.
Share bugs, ideas, or general feedback.
Persist Zustand store to localStorage or custom storage with automatic rehydration and migration support
persist middleware from zustand/middleware.name — this is the localStorage key. Make it unique per store.partialize to persist only specific fields. By default, the entire store is persisted.storage to change the storage backend (sessionStorage, IndexedDB, AsyncStorage).version and migrate to handle schema changes between app versions.// 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,
}),
}
)
);
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.
https://zustand.docs.pmnd.rs/middlewares/persist