Zustand: The React State Manager That Finally Makes Sense
After years of Redux boilerplate and Context re-render headaches, Zustand gave me global state that just works. Here's how I structure Zustand stores in real production apps.
I've used Redux (with and without Toolkit), MobX, Jotai, Recoil, and React Context for global state. Each has a use case. But for the medium-complexity apps I build most often — dashboards, CRM interfaces, multi-step forms — Zustand hits a sweet spot that none of the others matched: minimal boilerplate, no provider wrapping, and predictable re-renders by default.
The Minimal Store
import { create } from 'zustand';
interface AuthStore {
user: User | null;
token: string | null;
setUser: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()((set) => ({
user: null,
token: null,
setUser: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}));Selector-Based Subscriptions
The most important Zustand performance concept: components only re-render when the selected slice of state changes. Always select only what you need. A component that selects the entire store object will re-render on every state change. A component that selects only user.name re-renders only when the name changes.
// Re-renders on ANY store change — avoid this
const store = useAuthStore();
// Re-renders only when user.name changes — do this
const userName = useAuthStore((state) => state.user?.name);
// Select multiple values with shallow equality
const { user, logout } = useAuthStore(
useShallow((state) => ({ user: state.user, logout: state.logout }))
);Splitting Stores by Domain
Don't put everything in one store. I create separate stores per domain: useAuthStore, useCartStore, useUIStore (modals, sidebars, notifications), useFilterStore for complex filter state. Small, focused stores are easier to reason about and easier to test. Zustand stores compose naturally — one store can read from another using getState().
Persistence with zustand/middleware
import { persist, createJSONStorage } from 'zustand/middleware';
export const usePreferencesStore = create<PreferencesStore>()(
persist(
(set) => ({
theme: 'system',
language: 'en',
setTheme: (theme) => set({ theme }),
}),
{
name: 'user-preferences',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme, language: state.language }),
}
)
);When Not to Use Zustand
Zustand is overkill for server state. Don't use a Zustand store to cache API responses — that's what TanStack Query is for. The pattern I follow: TanStack Query for all server data (fetching, caching, mutations), Zustand for client-only state (UI state, user preferences, shopping cart contents). These two tools together cover 95% of state management needs without Redux.