Skip to Content
All posts

React Context vs Prop Drilling: When to Use Each (and When to Avoid Both)

 — #react#state-management#javascript#web-development

I've seen this happen a hundred times: a React codebase starts clean. Then features pile up. Props flow through five layers of components. Developers panic and reach for Context. Suddenly they've got three Context providers at the root, and changes to any of them re-render the entire app.

Both paths feel like failures. One feels tedious and wrong; the other feels smart but smells bad. Let me talk about what's actually happening and how to make a real choice.

The Real Problem with Prop Drilling

Prop drilling isn't inherently evil. What makes it annoying is intermediate components that don't care about the data. If a Button component needs to pass theme down to Text, but Button doesn't use theme itself, that's friction. You're using the component as a pipe, not a meaningful abstraction.

The pain points are real:

  • Refactoring becomes brittle (rename a prop and trace it through 10 components)
  • It's unclear which props matter to which level
  • New team members struggle to understand the data flow

But here's what I learned: drilling is a signal, not a sentence. If you're drilling the same thing through 3+ levels, ask why. Maybe:

  1. The intermediate component shouldn't exist (flatten the tree)
  2. You're drilling something that should live lower (move state closer to where it's used)
  3. You actually need a shared state mechanism (Context, Redux, Zustand)

When Context Actually Helps

Context shines when you have truly global or cross-cutting concerns:

  • Theme or dark mode toggle
  • Authenticated user info
  • Language/locale settings
  • UI state that affects many distant branches

The key: these things change infrequently, and when they do, re-rendering the whole tree is acceptable because the changes aren't granular.

Where Context fails is when you use it for frequently-changing, feature-specific state. Every time that context updates, every component reading it re-renders—even if they only use one field. You end up wrapping components in useMemo to prevent thrashing, which defeats the purpose.

I made this mistake. I threw a "GlobalState" context around the entire app with product filters, user preferences, UI modals, and notifications all in one object. When the user toggled a filter (which happened every few seconds), the entire app re-rendered. Then I added useMemo everywhere and the code became unreadable.

A Practical Heuristic

Ask yourself these questions:

1. Does this data change often?

  • If yes → use local state or a fine-grained state manager (Zustand, Jotai)
  • If no → Context is fine

2. How many components read this?

  • 1–2 → pass it as props
  • 3–4 → consider Context or local state in a shared parent
  • 5+ → reach for Context or a state manager

3. Is this the only thing in the context?

  • If no, split it → separate concerns into multiple contexts
  • If yes → you're probably good

4. Am I nesting Contexts more than 3 levels deep?

  • If yes → reconsider your structure or switch to a state manager

The Goldilocks Zone

Here's what worked for me:

  • Local state for component-specific toggles (expanded/collapsed, form inputs, hover state)
  • Prop drilling for 1–2 levels (Button → Text, Page → Card → Header)
  • Context for truly global stuff (theme, language, auth status)
  • Zustand or Jotai for feature-specific state that's used in multiple branches (filters, form state, UI modals)

The weird middle case? If you have state that lives in multiple components but isn't truly global—like a modal that's used in 3 unrelated parts of the app—don't use Context for the whole modal state. Instead:

  • Use Context for "which modal is open?" (boolean flag)
  • Use local state or a store for the modal's internal data
  • Pass callbacks or expose actions via the context

A Real Example

Say you're building a todo app with a sidebar filter panel and a main list. The filter changes every second as the user types. The list depends on the filter.

Bad: Context with { filter: string } → any filter change re-renders sidebar + list + buttons

Better:

// In a parent component
const [filter, setFilter] = useState("");
 
<FilterPanel filter={filter} onFilterChange={setFilter} />
<TodoList filter={filter} />

If 5 components need to read the filter, then you're justified reaching for Context or a store. Not before.

Honest Assessment

I've overcomplicated state management more times than I've nailed it. The pattern I've learned: start simple, observe the pain, then choose a tool. Not the other way around.

Prop drilling is annoying but honest—you can see the data flow. Context is powerful but can hide performance issues. Neither is wrong; context matters is starting with the simplest thing that works and only graduating when you feel real friction.

Try it on your next feature. Drill props for a week. Only reach for Context when you genuinely hate the drilling, not when you think you will.

— Mustaque Nadim