Here's a pattern that ships in nearly every component library: a provider at the root, a useContext hook inside each component, and a clean API that hides all the wiring. It looks like good abstraction. It often is. But at a certain scale, it becomes a performance liability that's genuinely hard to see until you're already in trouble.
Last week's issue was about how variant props accumulate complexity over time. Context providers have the same arc — they start clean and grow into something you didn't design.
The Problem Is Reference Identity, Not Context Itself
The core mechanic is worth stating precisely. As loserkid.io explains, useContext doesn't compare values — it compares references. When a provider re-renders and spreads a new state object, every consumer re-renders, regardless of whether the field it actually reads changed.
{} === {} // false
That's it. That's the whole problem. A provider holding twenty settings fields updates darkMode, and every component subscribed to that context wakes up — including the sixteen that only care about fontSize or locale. At small scale, this usually isn't catastrophic. In a design system with feature flags, theme tokens, and user preferences all flowing through a shared provider, it starts becoming expensive fast.
The failure mode compounds when you nest providers. Sethi's breakdown of React scale mistakes puts it plainly: one or two root-level providers is fine. Add theme, auth, feature flags, toast notifications, and a modal manager, and you've built a tree where a single theme change triggers React to walk the entire subtree looking for consumers. The providers themselves become the performance surface.
What Prop Drilling Actually Costs (and When It Costs Less)
The reflex is to reach for context the moment prop drilling gets annoying. That reflex is usually right — but "annoying" and "expensive" aren't the same thing.
Prop drilling at three levels is traceable. A new developer opens a component, sees user in the props, and can follow the chain. At eight levels across six feature modules, it becomes untraceable — not because the data is hidden, but because the path is too long to hold in your head. That's a real cost, but it's a comprehension cost, not a render cost.
Context inverts this. The data path is invisible by design — that's the point — but now you've introduced a render propagation surface that grows with every consumer you add. You traded one kind of cost for another. The question your design system should be asking is which cost is harder to pay at your current scale.
For most startup-scale systems, the answer is: comprehension costs hurt you sooner, render costs hurt you later. Reach for context. But scope it tightly.
Scope Is the Actual Discipline
The fix isn't "use context less." It's "use context closer to where the data is needed." Sethi's recommendation is direct: don't put context at the root unless the data genuinely belongs at the root. Wrap only the subtree that needs it.
// Don't put it at the root. Put it where the data is actually needed.
function DashboardSection() {
const user = useUser();
return (
<UserContext.Provider value={user}>
<DashboardContent />
</UserContext.Provider>
);
}
The intermediate components don't know user exists. The context update only propagates through the subtree that opted in. This is obvious in retrospect and almost never how design systems are initially built.
The slot pattern is another lever here. Ugur Aslim's writeup on component slots frames it as an alternative to both prop drilling and style-prop explosion: define structural regions in your component, let consumers fill them with their own JSX. The component defines where things go; consumers define what goes there. No context needed, no prop chain needed — just composition. It doesn't solve every problem, but for UI components that need flexibility without shared state, it sidesteps the context question entirely.
For cases where context is genuinely the right tool and render performance is becoming a real issue, the subscription pattern documented at loserkid.io is worth understanding: consumers subscribe to specific fields rather than the whole context value, so only affected components re-render. It's more infrastructure than most startup-scale systems need, but it's the right direction when you get there.
The Signal to Watch For
The moment your design system's root provider list starts growing — theme, auth, flags, modals, toasts — stop and ask whether each one actually needs to be at the root. Most don't. They're there because that's where providers go, not because that's where the data is needed.
Scope your providers to the subtree that consumes them. Prefer composition over context for UI flexibility. And when you do reach for context, treat the provider boundary as an architectural decision, not a wiring detail. The render cost is invisible until it isn't.
