Passing the same prop down through ten layers of components that don't even use it is called prop drilling, and it gets tedious fast. This lesson introduces React Context as the fix ā a way to create data that any component in the tree can read directly, no matter how deeply nested.
1The Problem
Imagine user data lives at the top of your app, but a tiny avatar component ten levels deep needs it. Passing that data down through every intermediate component as props ā even the ones that never use it themselves ā is called Prop Drilling, and it's tedious and messy to maintain.
Every component in the chain has to know about and forward a prop it has no actual use for, just so it can reach the one component at the bottom that does.
// Example
console.log("Running React...");Prop Drilling
Too deep.
2The Solution
React Context solves prop drilling by letting data 'teleport' directly to any component in the tree, completely bypassing everything in the middle. It effectively creates a global data layer that any component can tap into.
Instead of threading a prop through every intermediate layer, the components in between simply don't need to know the data exists at all ā only the top-level provider and the deeply nested consumer interact with it.
import { createContext } from 'react';
const ThemeContext = createContext('light');Global State
Direct Access.
3Step 1: createContext
Using Context starts with the createContext API, called once outside of any component: const ThemeContext = createContext('light'). This creates the Context object itself, which acts as the shared channel components will later provide to and read from.
The argument you pass in ā here, 'light' ā is an optional default value, used only if a component tries to read the context when there's no matching Provider above it in the tree.
function App() {
return (
<ThemeContext.Provider value='dark'>
<Navbar />
</ThemeContext.Provider>
);
}Creation
Initializing Context.
4Step 2: The Provider
Every Context object comes with a .Provider component. Wrapping <ThemeContext.Provider> around the part of your app that needs the data makes that data available to every component nested inside it, at any depth.
Crucially, you must pass a value prop to the Provider ā that's the actual data being broadcast, and without it, consumers inside the Provider would have nothing meaningful to read.
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}The Provider
Wrapping the tree.
5The Value Prop
The value prop on a Provider determines exactly what data gets broadcast to its children. Whatever you pass ā a string, an object, even multiple pieces of state bundled together like { theme, user } ā becomes what every nested consumer receives when it reads that context.
Any component inside the Provider, no matter how deeply nested, gets this exact value; there's no need to pass it through intermediate components as props at all.
// Automatic reactive updates
const [theme, setTheme] = useState('light');Payload
The shared data.
6Step 3: useContext
To actually read the data from deep within the tree, a component imports the useContext hook along with the Context object created earlier, then calls useContext(ThemeContext). This single call reaches up through the tree to the nearest matching Provider and returns its current value.
No intermediate component needs to pass anything down ā the consuming component just asks for the context directly, which is exactly what eliminates the prop drilling from the first lesson.
<h1>Context Master Unlocked!</h1>Consumption
Reading the Data.
7Step-by-Step Breakdown
The Problem. Imagine you have user data at the top of your app, but a tiny avatar component 10 levels deep needs that data. Passing props down through every intermediate component is called 'Prop Drilling'. It's tedious and messy.
The Solution. React Context is the solution. It provides a way to 'teleport' data directly to any component in the tree, completely bypassing the components in the middle. It creates a global data layer for your app.
What is the term for passing props through many layers of components that don't actually need the data themselves?
- āState Lifting
- āProp Drilling
Step 1: createContext. To use Context, you must first create it outside of your components using the createContext API. You can optionally pass a default value that will be used if a component tries to read the context outside of a Provider.
Step 2: The Provider. Every Context object comes with a .Provider component. You wrap this Provider around the parts of your app that need the data. Crucially, you MUST pass a value prop to the Provider.
The Value Prop. The value prop on the Provider determines what data is actually broadcasted to the children. Any component inside the Provider, no matter how deep, will receive this exact value when they ask for it.
What prop MUST you pass to a Context Provider to specify the data that will be broadcasted to its consumers?
- ādata
- āvalue
Step 3: useContext. To read the data from deep within the tree, we import the useContext hook and the Context object we created earlier. We pass the Context object into the hook.
Reactivity. Context is reactive! If the Provider's value changes (usually because it's tied to State), EVERY component using useContext for that specific Context will instantly re-render with the new data.
If a component calls useContext(MyContext), but there is NO <MyContext.Provider> anywhere above it in the tree, what value does it receive?
- āIt throws a runtime error
- āThe default value passed to createContext()
Updating from Consumers. How does a deep component update the Context? You pass the setState function itself down through the Context value alongside the data! Then the consumer can call it.
Performance Warning. A warning: When a Provider's value changes, EVERY consumer re-renders. If you put all your app state in one giant Context, your whole app re-renders on every tiny change. Split up your Contexts logically!
Context vs Redux. Context is great for low-frequency updates like Themes or Auth state. But for high-frequency data (like a trading app ticker) or complex state logic, tools like Redux or Zustand are better optimized.
Which of the following is generally considered a POOR use case for React Context due to performance concerns?
- āUser Theme (Light/Dark)
- āLive stock market ticker prices
Context Master Unlocked. Fantastic! You've unlocked the power of Global State in React. You know how to bypass prop drilling, create Providers, and consume data from anywhere. You're ready to architect complex applications!
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1A Theme Context Should Respect the User's Actual Preference
If a `ThemeContext` controls light/dark mode, initialize its value from `window.matchMedia('(prefers-color-scheme: dark)')` rather than hardcoding a default, so the app respects an OS-level accessibility preference from the very first render.
createContext(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')2Context-Driven UI Swaps Still Need Focus and Announcement Handling
When a value from Context (like an authenticated user or a locale) changes and causes a large part of the UI to swap out, treat it like any other major UI transition ā manage focus and consider an `aria-live` announcement rather than assuming Context makes the change automatically accessible.
SEO Implications
- 1
Context Is a Client-Side Data Mechanism With No Direct Effect on Crawlable HTML
React Context governs how components share data at runtime ā it doesn't change what ends up in the rendered HTML. What matters for SEO is whether the data ultimately rendered through Context is present in the server-rendered or statically generated output.
- 2
A Provider's Default Value Can Mask Missing Server-Side Data
If a Provider's value depends on data that hasn't loaded yet during server-side rendering, consumers may silently fall back to the `createContext` default instead of real content ā verify the actual data is available before the tree renders on the server.
Best Practices
Split Contexts by Concern Instead of One Giant Global Context
A single Context holding theme, auth, and app settings together means every consumer re-renders whenever any one of those changes. Separate `ThemeContext`, `AuthContext`, and similar keep re-renders scoped to what actually changed.
Reserve Context for Low-Frequency Global State
Context works well for things like theme or authentication status that change rarely. For high-frequency updates (like a live data feed) or complex interdependent state, a dedicated state management library is usually a better fit.
Frequent Bugs
A component reading useContext gets the default value instead of the data set by the Provider.
There's no matching `<Context.Provider>` anywhere above that component in the tree, so `useContext` falls back to whatever default was passed to `createContext()`. Make sure the component is actually rendered as a descendant of the intended Provider.
The entire app re-renders whenever any single piece of global state changes.
All app-wide data was bundled into one large Context, so every consumer re-renders on every update regardless of whether it cares about that particular piece of data. Split the state into multiple, narrowly scoped Contexts.
Real-World Examples
Sharing Authenticated User Data App-Wide
An app wraps its component tree in an `AuthContext.Provider` holding the current user and a way to update it, letting any nested component ā a navbar avatar, a settings page, a protected route check ā read the logged-in user without prop drilling.
const AuthContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
<Navbar />
<Dashboard />
</AuthContext.Provider>
);
}