React Context: Share State Without Prop Drilling
Use React Context to share state across components without prop drilling. When to reach for it, when not to, and live code to try it out.

You've got a theme value at the top of your app, and a button six levels deep needs to read it. So you pass theme to a layout, which passes it to a sidebar, which passes it to a panel, which passes it to a toolbar, which finally passes it to the button. Every component in that chain takes a prop it doesn't use just to hand it along. That's prop drilling, and it's miserable. Context is React's built-in fix: stash a value once, read it anywhere below, no relay race.
The problem, made concrete
Here's the relay race in code. App owns the theme, only the deepest component actually wants it, but Toolbar is forced to carry it through.
function App() {
const theme = "dark";
return <Layout theme={theme} />;
}
function Layout({ theme }) {
return <Toolbar theme={theme} />; // Layout doesn't use theme, just forwards it
}
function Toolbar({ theme }) {
return <ThemedButton theme={theme} />; // neither does Toolbar
}
function ThemedButton({ theme }) {
return <button className={theme}>Click</button>; // finally, someone uses it
}Two components exist purely to pass theme down. Add a second shared value (a logged-in user, say) and now every link in the chain carries two props it ignores. The chain gets fragile. Rename the prop and you touch every file. Insert a new component in the middle and you have to remember to thread the props through it too.
createContext, Provider, useContext
Context turns that vertical drilling into a direct line. The value skips the middlemen entirely:
Three pieces:
createContext(default)makes a context object. Call it once, usually in its own module.<MyContext.Provider value={...}>wraps part of your tree and supplies a value to everything inside it.useContext(MyContext)reads the nearest Provider's value from any component below, with no props required.
import { createContext, useContext } from "react";
const ThemeContext = createContext("light"); // "light" is the fallback
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />; // no theme prop in sight
}
function ThemedButton() {
const theme = useContext(ThemeContext); // reaches straight up to the Provider
return <button className={theme}>Click</button>;
}Toolbar is back to minding its own business. ThemedButton pulls theme directly from the context, skipping every component between it and the Provider. The default you pass to createContext only matters if a component reads the context with no Provider above it. Handy for tests, easy to forget.
Give your context a default that screams
For non-trivial contexts, set the default to something obviously wrong, like null or a function that throws "useTheme must be used inside ThemeProvider". A silent fallback that half-works is harder to debug than a loud failure that tells you exactly what you forgot.
A real theme switcher: context plus useState
A static value is the boring case. The point of context is sharing state that changes. Combine it with useState: put the state in a Provider component, and pass both the value and its setter down through context. Now any component can read the theme and flip it. Run this. The button and the banner are nowhere near each other in the tree, but they stay in sync.
Notice three things. The state lives in ThemeProvider, not scattered around. That's one source of truth. The value is an object holding both theme and toggle, so consumers can read and change it. And useTheme wraps useContext plus a guard, so every consumer (Banner, ToggleButton, Page) reads the same way and gets a clear error if it's used outside the Provider. That custom-hook wrapper is the standard pattern. It's exactly the kind of thing the custom hooks lesson set you up for.
ToggleButton and Banner are siblings, never pass props to each other, yet click the button and the banner updates instantly. They share state through context, not through a parent threading props down.
Quick check
When a Provider's value changes, which components re-render?
When NOT to use context
Context is a sharp tool, and people overuse it the week they learn it. It's for low-frequency, global-ish state: the theme, the current user, the chosen language, an auth token. Things many components need and that don't change every few milliseconds.
It's a bad fit when:
- The data is local. If only one component and its direct child need a value, just pass a prop. Two levels of prop passing is not prop drilling. It's normal React. Don't reach for context to avoid a single prop.
- It changes constantly. Every component reading a context re-renders when its value changes. Put a value that updates on every keystroke or every animation frame into a context wrapping half your app, and you'll re-render half your app. Keep fast-changing state local, or split it into a narrow context only the parts that care subscribe to.
- You're using it as a global variable dump. One giant context holding everything becomes the thing it was meant to replace, a tangle nobody can trace. Prefer a few small, focused contexts (
ThemeContext,AuthContext) over oneAppContext.
And for genuinely complex shared state (lots of writers, derived data, fine-grained subscriptions to avoid over-rendering) context alone gets awkward. That's where state libraries like Zustand or Redux Toolkit earn their keep. Context isn't a state manager. It's a way to deliver state without drilling. Reach for a library when delivery isn't your problem and managing the state is.
Context doesn't replace useState
Beginners sometimes think context is "better state." It isn't state at all — you still store state with useState (or useReducer) inside the Provider. Context only moves that state across the tree without props. No useState, no changing value.
Recap and what's next
Prop drilling is passing a value through components that don't use it just to reach one that does. Context kills it: createContext makes the channel, a <Provider value={...}> supplies a value to everything below, and useContext reads it from any depth, no relay. Pair it with useState inside a Provider to share changing state, and wrap the read in a small useTheme-style hook for a clean, guarded API. Just keep it for low-frequency global-ish data (theme, user, locale) and don't drop fast-changing or purely local state into it. For the full mental model, react.dev's Passing Data Deeply with Context is the canonical reference.
Next we step back from individual Hooks and look at how to assemble components themselves: composition patterns, covering children, slots, and building flexible components that don't lock you in.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


