Skip to main content

React State Management

My decision process for picking a state solution. The answer is almost always "use less state, not more libraries."


Decision Flowchart


My Rule of Thumb

Start local, lift only when needed. Most state never needs to leave the component.

ToolWhen I reach for it
useStateSingle value, simple toggle, form field
useReducerMultiple related fields, state machine-like logic
Context APITheme, locale, auth user — slow-changing, widely consumed
ZustandCross-component state that updates frequently (e.g. cart, filters, UI state)
Redux ToolkitLarge team, complex domain model, needs devtools + time-travel debugging

Context API: the re-render trap

Context re-renders every consumer when the value changes — even if they only use a small part of it.

// Bad: single context for everything
const AppContext = createContext({ user, cart, theme, filters });

// Better: split by update frequency
const AuthContext = createContext(user); // rarely changes
const CartContext = createContext(cart); // changes often → consider Zustand instead
const ThemeContext = createContext(theme); // rarely changes

Rule: Context is for state that changes rarely and is consumed widely (auth, theme, locale). If it changes on user interaction, use Zustand.


Zustand: my default for shared client state

import { create } from 'zustand';

interface CartStore {
items: CartItem[];
add: (item: CartItem) => void;
remove: (id: string) => void;
}

const useCartStore = create<CartStore>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
}));

Components only re-render when the specific slice they subscribe to changes. No providers, no prop drilling.


Server state is different

For async data from APIs, don't use any of the above — use a data-fetching library:

  • TanStack Query (React Query) — my default for REST
  • Apollo Client — if the API is GraphQL

These handle caching, background refetch, loading/error states. Replacing them with useEffect + useState is a mistake I've made and won't repeat.


Reference