State Management Patterns
The first sign of a senior engineer? They don't reach for a global state library by default. The second sign? They know exactly when they should.
The State Categorization Framework
Before choosing a tool, categorize your state:
| Type | Examples | Where it belongs |
|---|---|---|
| UI state | Modals, dropdowns, tab selection | Local component state |
| Form state | Input values, validation errors | Local or form library |
| Server state | API responses, cached data | React Query / SWR |
| URL state | Filters, pagination, search params | URL / router |
| Global app state | Auth, theme, feature flags | Context or external store |
Most "state management problems" are actually server state problems disguised as global state. React Query eliminated the need for Redux in 80% of applications.
Local State: The Default
useState and useReducer cover more ground than most engineers realize:
function useToggle(initial = false) {
const [state, setState] = useState(initial);
const toggle = useCallback(() => setState((s) => !s), []);
return [state, toggle] as const;
}
function useUndo<T>(initialValue: T) {
const [state, dispatch] = useReducer(
(s: { past: T[]; present: T }, action: { type: string; value?: T }) => {
switch (action.type) {
case 'SET':
return { past: [...s.past, s.present], present: action.value! };
case 'UNDO':
const prev = s.past[s.past.length - 1];
return { past: s.past.slice(0, -1), present: prev ?? s.present };
default:
return s;
}
},
{ past: [], present: initialValue }
);
return { state: state.present, set: (v: T) => dispatch({ type: 'SET', value: v }), undo: () => dispatch({ type: 'UNDO' }) };
}Context: Misunderstood and Misused
Context is a dependency injection mechanism, not a state management library. The issue: any context value change re-renders every consumer.
// Anti-pattern: one giant context
const AppContext = createContext({ user: null, theme: 'light', locale: 'en' });
// Changing theme re-renders every component reading user
// Better: split by update frequency
const AuthContext = createContext<User | null>(null);
const ThemeContext = createContext<'light' | 'dark'>('light');When Context Works Well
- Values that rarely change (theme, locale, auth)
- Scoped state for compound components
- Dependency injection for testability
When Context Falls Apart
- High-frequency updates (cursor position, animations)
- Deep trees with many consumers
- When you need selectors (consuming a slice of state)
Zustand: The Pragmatic Choice
Zustand hits the sweet spot for most applications â minimal API, no providers, built-in selectors:
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
add: (item: CartItem) => void;
remove: (id: string) => void;
total: () => number;
}
const useCart = create<CartStore>((set, get) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// Component only re-renders when items change
function CartCount() {
const count = useCart((s) => s.items.length);
return <span>{count}</span>;
}Zustand's selector model means components subscribe to slices â changing theme doesn't re-render components that only read items.
Redux Mental Model
Redux is overkill for most apps but its architecture teaches valuable concepts:
- Single source of truth â one store, one state tree
- State is read-only â mutations only via dispatched actions
- Pure reducers â
(state, action) â newState
Redux Toolkit removed the boilerplate problem. The remaining question is whether you need the indirection of actions and reducers, or whether direct mutations (Zustand, Jotai) are sufficient.
Decision Framework
Is it server data? â React Query / SWR
Is it URL-driven? â Search params / router state
Is it form data? â React Hook Form / local state
Is it used by 1-2 components? â useState / useReducer
Is it shared across distant components?
âââ Changes rarely? â Context
âââ Changes often? â Zustand / Jotai
Do you need time-travel / devtools / middleware? â Redux ToolkitInterview Signal
Senior candidates demonstrate:
- Categorization â identifying state types before choosing tools
- Trade-off awareness â knowing Context's re-render cost, Redux's indirection overhead, Zustand's simplicity trade-offs
- Server state separation â treating API data differently from UI state
- Minimal global state â defaulting to local, escalating only with justification