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​