Skip to Content
Module 9: Global & Server State9.1 State Management (Zustand)

Module 9: Global State Management & Mastering Zustand

Banner displaying a vibrant and abstract representation of state management concepts.

🎯 Learning Objectives

  • State Philosophy: Grasp when to use useState, Context API, or Zustand.
  • Bearbones API: Set up a global store in just 30 seconds.
  • Auto-Generating Selectors: Achieve automatic render performance optimization.
  • Middleware Power: Persist your state to LocalStorage indefinitely.
  • Async Actions: Handle asynchronous operations (like API calls) directly within your store.

1. State Philosophy: Local vs. Global

Diagram illustrating the difference between client-side and server-side data flow.

Not every piece of state needs to live in a Global Store. Overusing global state can make your application difficult to debug and lead to unnecessary re-renders.

State TypeCharacteristicsSolution
Local StateUsed only within a single component or its direct children. (e.g., Modal visibility, input field values, toggles)useState, useReducer
Global StateShared across many, geographically distant parts of your app. (e.g., User sessions, themes, shopping carts)Zustand, Context API
Server StateData fetched from APIs, requiring caching and re-fetching. (e.g., Product lists, user profiles)TanStack Query (Avoid using Zustand for this unless your app is very small)

2. Bearbones API: Lightning-Fast Stores

Zustand (German for “State”) is incredibly minimalist. Forget Provider components and boilerplate code like you’d find in Redux.

Flowchart illustrating the simple setup process for a Zustand store.

import { create } from 'zustand'; // 1. Define the Interface (TypeScript) interface BearState { bears: number; increase: () => void; removeAll: () => void; } // 2. Create the Store export const useBearStore = create<BearState>((set) => ({ bears: 0, increase: () => set((state) => ({ bears: state.bears + 1 })), removeAll: () => set({ bears: 0 }), }));

Use it anywhere:

import { useBearStore } from './useBearStore'; function BearCounter() { // Select state (Best Practice: Atomic Selector) const bears = useBearStore((state) => state.bears); return <h1>{bears} around here...</h1>; } function Controls() { // Select actions const increase = useBearStore((state) => state.increase); return <button onClick={increase}>One up</button>; }

3. Auto-Generating Selectors (Advanced)

Typing (state) => state.value repeatedly can become tedious. You can automatically generate use.x hooks to cut your code in half.

Illustration of how auto-generated selectors simplify state access.

Helper Utility (src/lib/createSelectors.ts):

import { StoreApi, UseBoundStore } from 'zustand'; type WithSelectors<S> = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never; const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => { let store = _store as WithSelectors<typeof _store>; store.use = {}; for (let k of Object.keys(store.getState())) { (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); } return store; }; export default createSelectors;

Applying it to Your Store:

import { create } from 'zustand'; import createSelectors from '../lib/createSelectors'; interface SettingsState { theme: 'dark' | 'light'; toggle: () => void; } const useSettingsBase = create<SettingsState>((set) => ({ theme: 'dark', toggle: () => set((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })), })); // Wrap the store export const useSettingsStore = createSelectors(useSettingsBase);

Using it (Super concise):

// ❌ Old way: const theme = useSettingsStore((state) => state.theme) // ✅ New way: Each field automatically gets its own hook! const theme = useSettingsStore.use.theme(); const toggle = useSettingsStore.use.toggle(); // Note: You still call the action directly

4. Middleware Power: Persist (Save to LocalStorage)

Automatically save your state (like your shopping cart or theme settings) to LocalStorage so it’s not lost when you refresh the page.

Diagram showing how data is persisted to LocalStorage.

import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; interface CartState { items: string[]; isLoading: boolean; // This state should NOT be persisted addItem: (item: string) => void; } export const useCartStore = create<CartState>()( persist( (set) => ({ items: [], isLoading: false, addItem: (item) => set((state) => ({ items: [...state.items, item] })), }), { name: 'cart-storage', // Key name in LocalStorage storage: createJSONStorage(() => localStorage), // ✅ Partialize: Only persist 'items', exclude 'isLoading' partialize: (state) => ({ items: state.items }), } ) );

5. Async Actions

Zustand handles async/await natively, eliminating the need for complex middleware like redux-thunk or redux-saga.

Flowchart illustrating an asynchronous operation within the store.

interface AuthState { user: User | null; isLoading: boolean; login: (email: string) => Promise<void>; } export const useAuthStore = create<AuthState>((set) => ({ user: null, isLoading: false, login: async (email) => { set({ isLoading: true }); // 1. Start loading try { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email }) }); const user = await response.json(); set({ user }); // 2. Success } catch (error) { console.error(error); } finally { set({ isLoading: false }); // 3. Finish } }, }));

6. Key Takeaways

Zustand is the go-to choice for client-side state management in React today, thanks to its:

  1. Simplicity: Learn it in under 5 minutes.
  2. Power: Full middleware support, DevTools integration, and robust TypeScript capabilities.
  3. Performance: Efficient selectors prevent unnecessary re-renders.

7. Further Reading

Last updated on