State Management Patterns

Difficulty: Advanced

Question

How do you decide between different state management approaches? Compare Context API, Redux, and Zustand.

Answer

State management is one of those topics where there's no single right answer, and showing that you understand the trade-offs is way more impressive in an interview than just saying "I use Redux" or "I use Zustand." So let's walk through the entire landscape and build an intuition for when to reach for what.

First, let's acknowledge something that took the React community years to figure out: not all state is the same. There's a fundamental difference between client state (things like "is this modal open," "what did the user type in this input," "which tab is selected") and server state (data that lives on a backend - user profiles, product lists, comments). These two categories have completely different characteristics and deserve different solutions. Server state is asynchronous, can become stale, is shared across users, and needs caching, refetching, and synchronization. Client state is synchronous, local, and controlled entirely by the user's interactions. Mixing them together in one big global store is a recipe for unnecessary complexity.

Let's start with the simplest option: useState. This is your default. If a piece of state is used by exactly one component and doesn't need to be shared anywhere, useState is the answer. Form inputs, toggles, modal visibility, local loading states - all perfect candidates for useState. A surprising number of developers jump straight to global state management when useState would have been perfectly fine. Keep things local until you have a reason not to.

Next up: lifting state. When two sibling components need to share state, you move the state up to their nearest common parent and pass it down via props. This is basic React data flow. It works great when you have a parent with two or three children that need the same data. The parent owns the state, passes the value down to one child and an onChange callback to the other.

But what about when you need to share state across many levels of the component tree? That's where prop drilling becomes painful - passing props through five or six intermediate components that don't even use the data. This is where Context API enters the picture.

The Context API lets you create a "wormhole" in the component tree. You wrap a Provider around a section of your tree, give it a value, and any descendant - no matter how deeply nested - can consume that value directly without props being passed through every layer. It's fantastic for things like theme (light/dark mode), authentication (current user), locale/language, and feature flags - data that changes infrequently and is needed by many components.

But Context has a significant limitation that catches people off guard: it has no concept of selectors. When the context value changes, every single component that consumes that context re-renders. Every one. Even if the component only cares about one tiny piece of the context value. So if you have a context that holds { user, theme, notifications } and the notification count updates every 30 seconds, every component that reads the user or theme from that same context will also re-render. For infrequent changes like theme toggles, this is totally fine. For frequently updating data like a search query or a live counter, it becomes a performance problem.

This is exactly where libraries like Zustand shine. Zustand is a minimal state management library that gives you a global store with built-in selector support. When you write useStore(state => state.count), your component only re-renders when count changes - not when any other piece of the store changes. And you don't need a Provider component wrapping your tree. It's just a hook you call. The API surface is tiny, there's almost no boilerplate, and it handles most client-state needs beautifully.

Redux is the grandparent of React state management. It's battle-tested, has incredible devtools (time-travel debugging, action logging), and a massive ecosystem of middleware. But traditional Redux was notorious for boilerplate - action types, action creators, reducers, switch statements everywhere. Redux Toolkit (RTK) dramatically reduced this boilerplate and is the officially recommended way to use Redux today. If you're on a large team working on a complex application where you need strict patterns, middleware for async operations, and robust devtools, Redux Toolkit is still a solid choice. But for most applications, it's more ceremony than you need.

And then there's the server state category, which changed the game entirely. Libraries like React Query (TanStack Query) and SWR handle everything related to fetching, caching, synchronizing, and updating server data. They manage loading states, error states, stale-while-revalidate caching, background refetching, optimistic updates, pagination, infinite scroll - all the things that people used to stuff into Redux stores with mountains of custom code. If you're building a modern React app and you're putting API response data into Redux or Zustand, you're almost certainly doing too much work. Let React Query handle server state, and use Zustand or Context for the small amount of client state you actually have.

So here's my mental decision tree for a real project. Is it server data from an API? Use React Query. Is it local to one component? useState. Shared between a parent and a couple children? Lift state up. Needed app-wide but changes rarely, like theme or auth? Context API. Needed app-wide with frequent updates or complex logic? Zustand. Need strict patterns, middleware, devtools, and you have a large team? Redux Toolkit. Most real-world apps end up combining two or three of these - typically React Query for server state plus Zustand or Context for client state.

The key insight for interviews is showing that you think in terms of trade-offs, not loyalties. You're not a "Redux developer" or a "Zustand developer." You're someone who picks the right tool for the specific type of state you're dealing with.

Code examples

Context API - When It Works

// Good for: theme, auth, locale (infrequent changes)
const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Problem: ALL consumers re-render
// even if they only use 'theme' and 'setTheme' changes
function useTheme() {
  return useContext(ThemeContext);
}

Context is great for low-frequency updates. For high-frequency updates (e.g., typing in a search box), it causes excessive re-renders.

Zustand - Minimal & Efficient

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  name: 'John',
  increment: () => set((state) => ({ count: state.count + 1 })),
  setName: (name) => set({ name }),
}));

// Selector: only re-renders when count changes
function Counter() {
  const count = useStore((s) => s.count);
  const increment = useStore((s) => s.increment);
  return <button onClick={increment}>Count: {count}</button>;
}

// This component does NOT re-render when count changes
function NameDisplay() {
  const name = useStore((s) => s.name);
  return <p>Name: {name}</p>;
}

Zustand supports selectors out of the box. Components only re-render when their selected slice of state changes. No provider needed.

When to Use What (Decision Tree)

// 1. Is it server data (API responses)?
//    → Use React Query / TanStack Query

// 2. Is it used by only one component?
//    → useState

// 3. Is it shared between a parent and 2-3 children?
//    → Lift state up + props

// 4. Is it app-wide but changes rarely (theme, auth)?
//    → Context API

// 5. Is it app-wide with frequent updates or complex logic?
//    → Zustand (simple) or Redux Toolkit (complex)

// 6. Does it need middleware, time-travel, or devtools?
//    → Redux Toolkit

// Real-world: most apps use a combination
// e.g., React Query for server state + Zustand for UI state

There's no one-size-fits-all. The best approach combines multiple tools for different types of state.

Key points

Concepts covered

Context API, Zustand, Redux, Prop Drilling, State Lifting