Hero image for "The Consistency Problem Hiding Inside Your Context API"

The Consistency Problem Hiding Inside Your Context API


Here's a scene worth sitting with: one developer opens a ticket, finds a piece of state that needs to travel four levels down the component tree, and reaches for React Context. Another developer, same codebase, same week, hits the identical structural problem and passes props through four levels without comment. A third wraps the whole thing in a small state library that nothing else in the project touches.

All three solutions in the same codebase. No shared standard. No signal about which one applies when.

This is the actual prop drilling problem — and it has almost nothing to do with prop drilling.

The Decision Is Cheap. The Inconsistency Is Expensive.

Prop drilling gets framed as a performance or readability problem, and sometimes it is. But the deeper issue in most startup codebases is that the choice between drilling, Context, and composition is made fresh every time someone encounters the pattern. There's no project-level rule. So the codebase accumulates four different answers to the same question, and the next developer who touches any of those components has to reverse-engineer which philosophy applies before they can safely change anything.

The underlying mechanics aren't complicated: prop drilling passes data explicitly through intermediate components that don't use it; Context makes data available to any descendant without threading it through the tree. Both are legitimate. The problem is that "legitimate" and "consistent" are different things, and design systems live or die on the second one.

Context is the right call when data is genuinely global to a subtree, changes infrequently, and doesn't need fine-grained update control. Composition — passing components as children rather than data as props — is the right call when intermediate components are structurally in the way but don't actually need the data. Straight prop drilling is defensible when the chain is short and the data is local. A state management library makes sense when update logic is complex or state needs to reach many unrelated parts of the tree.

None of these is wrong. All of them, applied without a shared rule, create a codebase where every component is a small archaeology project.

Abstraction Layers Don't Kill Velocity. Premature Ones Do.

The instinct to reach for Context early is understandable. It feels like the sophisticated answer — you're solving the problem "properly" instead of just threading props. But sophistication has a cost, and that cost compounds.

Design Systems Collective put it plainly: when you create a reusable abstraction, you're not just writing a component — you're creating an API. That API needs to support multiple use cases, handle flexible props, and avoid breaking every consumer when requirements shift. The same logic applies to Context. A Context provider isn't just a data conduit; it's an architectural commitment about what counts as "global" in your subtree. Make that call wrong and you've got a provider that wraps components that don't need it, re-renders that happen for the wrong reasons, and a mental model that doesn't match the actual data flow.

The composition pattern sidesteps a lot of this. When an intermediate component doesn't need the data itself — it's just structurally between the producer and the consumer — passing a component as a child instead of passing data as a prop eliminates the drilling without introducing a shared state layer. The data never travels through the intermediate component at all. It's a smaller commitment than Context, easier to refactor, and it keeps the component's API honest about what it actually needs.

The failure mode is reaching for Context because it feels cleaner, then discovering six months later that your ThemeContext is triggering re-renders across a subtree that only two components actually care about.

The Rule Your Codebase Needs

The fix isn't picking the "right" pattern. It's picking a rule and writing it down.

Something like: prop drilling is fine up to three levels; beyond that, evaluate whether composition solves it before reaching for Context; use Context only when data is genuinely shared across a subtree and changes infrequently; introduce a state library only when Context's update model isn't granular enough.

That's not a universal law — it's a starting point that your team can argue with, refine, and actually apply consistently. The problem with having no rule is that every session makes an independent judgment call, and session-to-session variance is almost guaranteed. The codebase ends up encoding four different opinions about the same question, and none of them are wrong enough to justify a refactor.

Shopify Polaris and GOV.UK are worth studying here — not for their component APIs, but for the fact that their documentation explains when and why to use a pattern, not just what it looks like. That's the transferable practice: the decision criteria, not the decision.

The goal isn't a codebase where everyone uses Context. It's a codebase where everyone uses Context for the same reason.