patterntypescriptreactModerate
Zustand middleware — persist and devtools
Viewed 0 times
zustand persistzustand devtoolsmiddlewarelocalStoragepartializecreateJSONStoragedouble call syntax
Problem
Zustand stores reset on page refresh by default. Adding localStorage persistence and Redux DevTools support requires middleware, but composing multiple Zustand middlewares with TypeScript produces complex generic types that are hard to write correctly.
Solution
Compose devtools and persist using the pipe pattern; let TypeScript infer types from the callback:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (lang: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
devtools(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }, false, 'setTheme'),
setLanguage: (lang) => set({ language: lang }, false, 'setLanguage'),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
// Persist only specific keys:
partialize: (state) => ({ theme: state.theme, language: state.language }),
}
),
{ name: 'SettingsStore' }
)
);
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (lang: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
devtools(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }, false, 'setTheme'),
setLanguage: (lang) => set({ language: lang }, false, 'setLanguage'),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
// Persist only specific keys:
partialize: (state) => ({ theme: state.theme, language: state.language }),
}
),
{ name: 'SettingsStore' }
)
);
Why
The outer function call syntax — create<T>()(...) — is required when using middleware so TypeScript can properly infer the state type. The partialize option prevents actions (which are functions) from being serialised to localStorage.
Gotchas
- Do NOT persist functions/actions — use partialize to select only serialisable state
- The double-call syntax create<T>()(...) is needed with middleware for correct TypeScript inference
- devtools third argument to set is the action name shown in Redux DevTools — always provide it for debuggability
- Storage keys are global — two stores using the same name key will overwrite each other
- When the persisted schema changes, old localStorage values may cause errors — implement a migrate option
Revisions (0)
No revisions yet.