From harness-claude
Persists Zustand store to localStorage or custom storage with automatic rehydration and migration support for state survival across page reloads.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:state-zustand-persistThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Persist Zustand store to localStorage or custom storage with automatic rehydration and migration support
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
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeImplements Zustand middleware for persistence (persist), devtools integration, Immer immutability, and custom store enhancements. Guides composition, best practices, partialize, and migrations.
Creates and manages Zustand stores with selectors, persistence, devtools, and middleware. Useful for global state in React or vanilla JavaScript.
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.