Zustand state management conventions -- stores, selectors, middleware, persist, devtools. Use when project has zustand in package.json.
Detection: Check package.json for zustand. If absent, skip.
import { create } from 'zustand';
interface AuthStore {
user: User | null;
token: string | null;
login: (credentials: LoginDto) => Promise<void>;
logout: () => void;
setUser: (user: User) => void;
}
export const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
login: async (credentials) => {
const { user, token } = await api.login(credentials);
set({ user, token });
},
logout: () => set({ user: null, token: null }),
setUser: (user) => set({ user }),
}));
Select only what you need — components re-render only when their selected value changes:
// Good — component re-renders only when user changes
const user = useAuthStore((state) => state.user);
const login = useAuthStore((state) => state.login);
// Bad — component re-renders on ANY store change
const store = useAuthStore();
For multiple selectors, use useShallow to prevent unnecessary re-renders:
import { useShallow } from 'zustand/react/shallow';
const { user, token } = useAuthStore(useShallow((state) => ({
user: state.user,
token: state.token,
})));
import { persist, createJSONStorage } from 'zustand/middleware';
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'system' as 'light' | 'dark' | 'system',
locale: 'en',
setTheme: (theme) => set({ theme }),
setLocale: (locale) => set({ locale }),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme, locale: state.locale }), // persist only these fields
},
),
);
import { devtools } from 'zustand/middleware';
export const useCartStore = create<CartStore>()(
devtools(
(set) => ({ /* store definition */ }),
{ name: 'CartStore' }, // label in Redux DevTools
),
);
// persist + devtools
export const useStore = create<Store>()(
devtools(
persist(
(set) => ({ /* ... */ }),
{ name: 'store' },
),
{ name: 'Store' },
),
);
interface OrderStore {
orders: Order[];
loading: boolean;
error: string | null;
fetchOrders: () => Promise<void>;
}
export const useOrderStore = create<OrderStore>((set) => ({
orders: [],
loading: false,
error: null,
fetchOrders: async () => {
set({ loading: true, error: null });
try {
const orders = await api.getOrders();
set({ orders, loading: false });
} catch (e) {
set({ error: e instanceof Error ? e.message : 'Unknown error', loading: false });
}
},
}));
useStore() without selector re-renders on every change. Always use selectors.set({ items: [...state.items, newItem] }) not state.items.push(newItem).useShallow — selecting object/array without shallow comparison causes unnecessary re-renders.partialize to exclude sensitive fields.useAuthStore, useCartStore, useUIStore.create<StoreType>() with extra () is needed when using middleware.zustand — stores, middleware, selectors, persist, devtools