Zustand for client state (and hydrating from the server)
Zustand offers a minimal API for client-side global state: no providers required, good TypeScript inference with slices, and middleware for logging, devtools, or persistence.
Basic store with slices
import { create } from 'zustand';
type UiState = {
sidebarOpen: boolean;
toggleSidebar: () => void;
};
export const useUiStore = create<UiState>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
Use selectors in components: const open = useUiStore((s) => s.sidebarOpen) to avoid unnecessary re-renders.
When not to use Zustand
Server-fetched page data belongs in React Query or Server Components, not a global store duplicated from props. Stores excel for UI chrome and cross-route client state (modals, wizards, theme toggles).
Hydrating from the server
Pass serialized initial state from a Server Component into a small client bootstrap:
'use client';
import { useRef } from 'react';
import { useUiStore } from './store';
export function HydrateUi({ initial }: { initial: Partial<UiState> }) {
const once = useRef(false);
if (!once.current) {
useUiStore.setState(initial);
once.current = true;
}
return null;
}
Guard against double-invocation in Strict Mode when mutating global singletons.
persist middleware
LocalStorage persistence is convenient for drafts; remember:
- SSR: default storage is
undefined—useskipHydrationpatterns from docs. - Sensitive data should never be persisted in plain text.
Summary
Zustand fits small, explicit client stores. Combine with server-first data loading and keep persistence scoped to non-sensitive UI preferences unless you add encryption and threat modeling.