Context & State Sharing
Avoiding prop drilling without accidentally re-rendering your whole tree.
35 minDifficulty 3/5react · stateAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A theme value needs to reach a deeply nested button, six components down, and none of the components in between actually care about it — they just have to accept it as a prop and pass it along, cluttering every layer's interface with data they don't use. Context lets a value skip straight from a provider to whatever descendant actually needs it, without every intermediate component knowing it exists.
The mental model
Think of context as a value broadcast down a specific branch of the component tree. A `Provider` component sets the value for everything nested inside it; any descendant, no matter how deep, can read it with `useContext` — as if it were a global, but scoped to that branch of the tree rather than the whole app.
How it works
Context solves prop drilling, not general state management
Prop drilling is passing a value through components that don't use it, purely to reach one that does. Context removes that pass-through burden. It doesn't, on its own, solve where the state lives or how it changes — you still typically pair it with `useState` or `useReducer` somewhere above the provider to actually own and update the value.
Every consumer re-renders when the context value changes
When a Provider's value prop changes, EVERY component that calls `useContext` for that context re-renders — regardless of whether it reads the specific part of the value that changed. This is fine for infrequently-changing values like a theme, but a context updating on every keystroke can re-render a much larger part of the tree than expected.
A new object literal as the value prop breaks memoization
`<Context.Provider value={{ theme, setTheme }}>` creates a brand-new object on every render of the provider component, even if `theme` itself hasn't changed — every consumer sees a 'different' value by reference and re-renders unnecessarily. Wrapping the value in `useMemo` keyed on the actual dependencies avoids this.
Splitting contexts limits the blast radius of a change
Bundling frequently-changing data (like cursor position) and rarely-changing data (like a theme) into one context means every re-render of the frequent value also re-renders every consumer of the theme. Splitting them into two separate contexts lets each set of consumers re-render only when the specific data they actually use changes.
The mechanism
A Provider component wraps a subtree and supplies a value. Any descendant anywhere in that subtree — regardless of nesting depth, and regardless of whether the components in between reference the context at all — can call useContext to read the current value directly, bypassing every intermediate layer's props entirely.
flowchart TD P[Provider value=theme] --> A[Component A] P --> B[Component B] B --> C[Component C - useContext] P --> D[Component D - useContext]
What people get wrong
- Context is a general-purpose global state management solution.
- Context solves the specific problem of passing a value through many layers without prop drilling — it doesn't provide selective subscriptions, so every consumer re-renders on any change, which doesn't scale well to frequently-updating, complex application state. Reaching for context as the default state solution for everything leads to performance problems once the shared state updates often, because context lacks the fine-grained subscription that a dedicated state library provides.
- Only the specific components reading the changed part of a context's value will re-render.
- Every component calling useContext for that context re-renders when the Provider's value changes, even if the component only reads a field of the value object that didn't itself change. This is the most common surprise when a seemingly-unrelated part of the UI re-renders after an unrelated piece of context data updates, especially when the whole context value is one bundled object.
- Passing an object literal directly as the value prop is harmless.
- An inline object literal is a new reference on every render of the provider, defeating any memoization consumers rely on and causing unnecessary re-renders across the whole subtree. This is easy to write without noticing, because the object's actual field VALUES might be unchanged even though the object itself is a new reference each time.
When not to use it
- The value changes very frequently (multiple times per second) and many components consume it.
- A dedicated state management library with selective subscriptions, or restructure so the frequently-changing value lives closer to where it's actually used, rather than in a broad context.
- The value only needs to reach one or two levels down.
- Plain prop passing — context adds indirection that isn't worth it when the components in between genuinely need the value too, or when the tree is shallow.
Terms
- Prop drilling
- — Passing a value through multiple components that don't use it themselves, purely to reach a deeply nested descendant that does.
- Provider
- — The component that supplies a context's value to everything nested within it.
- Consumer
- — Any component that reads a context's current value, typically via the useContext hook.
- Context split
- — The practice of dividing data into multiple separate contexts based on how frequently each part changes, to limit unnecessary re-renders.
Can you recall it?
Why does wrapping a Provider's value in useMemo matter, and what specifically goes wrong without it?