What Is React Context? Sharing Data Without Prop Drilling
React Context is the PA system for your app — broadcast data to every component that needs it, without passing notes person to person through the chain.
TL;DR
React Context is React's built-in way to share data — like theme, user info, or language settings — across your entire app without passing props through every single component in between. You create a context with createContext, wrap your component tree in a Provider, and then any child component can grab that data with the useContext hook. AI generates this pattern constantly. If you've seen ThemeContext.Provider or useContext(AuthContext) in your code and didn't know what it was doing — this article is for you.
Why AI Coders Need to Know This
You ask Claude or Cursor to "add a dark mode toggle" and it generates three files you didn't expect. There's a ThemeContext.js with createContext in it. There's a ThemeProvider component wrapping your entire app. And there's a useContext(ThemeContext) hook buried inside a button component three levels deep.
You didn't ask for any of that. You asked for a toggle. But the AI knows something you might not: the toggle needs to change the theme everywhere, not just in one component. Context is how React does that.
Here's the problem: if you don't understand what Context is, you can't:
- Figure out why the toggle "works" but the theme doesn't actually change across pages
- Debug the
Cannot read properties of undefinederror that happens when the Provider is missing - Know whether Context is the right tool — or if you should be using Zustand instead
- Give the AI useful feedback when the generated code doesn't behave correctly
Context shows up in almost every non-trivial React app that AI generates. Understanding it is non-negotiable if you're building real things.
Real Scenario
The problem: prop drilling
Imagine a job site with ten subcontractors. The foreman gets a change order: "We're switching to matte finish paint, not gloss." Now the foreman has to tell the lead painter, who tells each painter individually, who tells their helpers. Every person in the chain has to carry the message, even the ones who don't paint at all — like the electrician who's standing between the foreman and the paint crew.
That's prop drilling in React. Data starts at the top component and gets passed down through every intermediate component — even the ones that don't use it — just so a deeply nested child can access it.
Now imagine the foreman just grabs the PA system and announces: "All teams — switch to matte finish." Everyone who needs to hear it, hears it. Nobody has to relay the message. That's React Context.
Your prompt to the AI
"Add a dark mode toggle to my React app. The toggle should be in the navbar, and the theme should apply to every page and component."
This prompt requires the current theme to be available everywhere — navbar, page layout, individual cards. Without Context, you'd pass theme and setTheme as props through every component between App and each leaf component. That's prop drilling — and AI knows to avoid it.
What AI Generated
Here's what a typical AI response generates when you ask for a dark mode toggle. It creates three pieces:
Step 1: Create the Context and Provider
// ThemeContext.jsx
import { createContext, useState, useContext } from 'react';
// 1. Create the context (the "PA system")
const ThemeContext = createContext();
// 2. Create the Provider (the "foreman with the microphone")
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Create a custom hook (convenient shortcut)
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Step 2: Wrap the app in the Provider
// App.jsx
import { ThemeProvider } from './ThemeContext';
import Navbar from './Navbar';
import MainContent from './MainContent';
function App() {
return (
<ThemeProvider>
<Navbar />
<MainContent />
</ThemeProvider>
);
}
Step 3: Use the context anywhere
// Navbar.jsx — the toggle lives here
import { useTheme } from './ThemeContext';
function Navbar() {
const { theme, toggleTheme } = useTheme();
return (
<nav className={`navbar ${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙 Dark Mode' : '☀️ Light Mode'}
</button>
</nav>
);
}
// Card.jsx — a deeply nested component, no prop drilling needed
import { useTheme } from './ThemeContext';
function Card({ title, content }) {
const { theme } = useTheme();
return (
<div className={`card ${theme}`}>
<h3>{title}</h3>
<p>{content}</p>
</div>
);
}
Notice what's not happening: MainContent doesn't receive theme as a prop. Neither does any component between App and Card. The Card reaches up and grabs the theme directly from the Context. No relay chain. No prop drilling.
Understanding Each Part
createContext() — Installing the PA system
createContext() creates the communication channel. It doesn't hold any data yet — it just sets up the infrastructure. Think of it as installing the PA speakers on the job site. The speakers are there, but nobody's talked into the microphone yet.
You can optionally pass a default value: createContext('light'). This default is used only when a component calls useContext but there's no Provider above it in the tree. In practice, you almost always have a Provider, so the default rarely matters — but it helps with TypeScript and testing.
Provider — The foreman with the microphone
The Provider is a component that wraps part of your app and says: "Everyone inside me can access this data." The value prop is what gets broadcast — in our case, the current theme and the toggle function.
The Provider must be an ancestor of any component that wants to use the context. If a component tries to use useContext(ThemeContext) but isn't inside a ThemeProvider, it gets undefined. This is the #1 Context bug AI creates — and we'll cover it below. Usually you wrap your entire app in the Provider, but you can also wrap just a section if needed.
useContext() — Listening to the PA system
The useContext hook is how any component tunes into the broadcast. You pass it the context object (like ThemeContext), and it returns whatever the nearest Provider has in its value prop.
When the Provider's value changes (someone toggles the theme), every component using useContext(ThemeContext) automatically re-renders with the new value. You don't have to do anything — React handles the update.
The custom hook pattern — useTheme()
You'll notice AI almost always creates a custom hook like useTheme() instead of having components call useContext(ThemeContext) directly. This is a best practice for two reasons:
- Error handling: The custom hook can check if the context is undefined and throw a helpful error message ("useTheme must be used within a ThemeProvider") instead of a cryptic "Cannot read properties of undefined"
- Cleaner imports: Components only import
useTheme— they don't need to know aboutThemeContextoruseContextat all
If AI generates a custom hook wrapper for the context, that's a sign of good code generation. Keep it.
Context vs Zustand: When to Use Which
AI sometimes generates Context when Zustand would be better, and vice versa. Here's the quick decision framework:
The Rule of Thumb
Context = data that changes rarely and many components need to read. Zustand = data that changes frequently or where performance matters.
Use React Context when:
- Theme / dark mode — changes maybe once per session
- Authenticated user — changes on login/logout only
- Language / locale — changes rarely
- Feature flags — set once on app load
Use Zustand (or another state manager) when:
- Shopping cart — items added/removed frequently, multiple components reading and writing
- Form state across pages — multi-step wizards where state needs to persist across route changes
- Real-time data — chat messages, notifications, live counters
- Any state that updates multiple times per second — Context re-renders every consumer on every change
Why the difference matters
When a Context Provider's value changes, every component that calls useContext on that context re-renders — even if the specific piece of data that component cares about didn't change. If your context holds {{ theme, user, cart, notifications }} and the notification count updates, the theme toggle re-renders too. For data that changes once or twice per session, this isn't a problem. For data changing every second, it kills performance.
Zustand solves this with selectors — a component subscribes to just the slice it needs: useStore(state => state.cartCount). It only re-renders when cartCount changes. If AI puts a shopping cart in Context and your app feels sluggish, tell it: "Refactor the cart state from Context to Zustand with selectors for performance."
What AI Gets Wrong
1. Missing Provider — the #1 Context bug
AI generates the context file and the consuming components but forgets to wrap the app in the Provider. Or it wraps only one route and the context is undefined on other routes. You'll see:
TypeError: Cannot read properties of undefined (reading 'theme')
The fix: Make sure the Provider wraps everything that needs the context. Usually that means wrapping your entire <App /> in main.jsx or index.jsx. Tell the AI: "Wrap the ThemeProvider around the entire App component in the entry point file."
2. Stuffing too much into one Context
AI loves to create a single "AppContext" that holds theme, user, cart, preferences, and notifications all in one. This works — until performance tanks because every tiny update re-renders everything.
The fix: Split into separate contexts: ThemeContext, AuthContext, CartContext. Each one only triggers re-renders for the components that actually use it. Tell the AI: "Split the AppContext into separate contexts for theme, auth, and cart."
3. Creating a new object every render
This is subtle. Look at this Provider:
<ThemeContext.Provider value={{ theme, toggleTheme }}>
Every time this component re-renders, {{ theme, toggleTheme }} creates a new object. React sees a new object and tells every consumer to re-render — even if theme didn't actually change. For simple apps this is fine. For complex apps it causes unnecessary re-renders.
The fix: Wrap the value object in useMemo:
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
Tell the AI: "Memoize the context value to prevent unnecessary re-renders."
4. Stale context values in async callbacks
This is the same stale closure problem that haunts useEffect. If you capture a context value inside a setTimeout or fetch callback, the callback remembers the value at the time it was created, not the current value.
The fix: Use useRef to keep a reference to the latest value, or restructure so the async logic doesn't need to read stale context. Tell the AI: "The context value is stale inside my async callback. Use a ref to track the latest value."
5. Using Context for frequently-changing state
AI might put a real-time message counter or a mouse position tracker in Context. Since Context re-renders all consumers on every update, this causes your entire UI to flicker and lag.
The fix: Move frequently-changing state to Zustand or another state manager with selector support. Tell the AI: "This state updates too frequently for Context. Refactor it to Zustand with granular selectors."
How to Debug Context Issues
The "undefined context" error
Symptom: Cannot read properties of undefined when you call useContext.
What happened: The component isn't inside a Provider.
Debugging prompt:
Debug prompt
"I'm getting 'Cannot read properties of undefined' when using useContext(ThemeContext). I think the ThemeProvider is missing or not wrapping this component. Can you check my component tree and make sure the Provider is in the right place?"
Theme changes don't propagate
Symptom: Clicking the toggle updates the button text but the rest of the app doesn't change.
What happened: Components are reading theme from somewhere else (local state, CSS class, hardcoded) instead of from Context.
Debugging prompt:
Debug prompt
"My dark mode toggle works in the navbar but other components don't update. I think they're not using useContext to read the theme. Can you audit all components and make sure they read theme from ThemeContext instead of local state or props?"
Performance issues — everything re-renders
Symptom: The app feels sluggish. React DevTools shows many components re-rendering when Context updates.
What happened: Too much state in one Context, or the value object isn't memoized.
Debugging prompt:
Debug prompt
"My app re-renders too many components when the theme changes. I think the context value object is being recreated every render. Can you memoize it with useMemo and also check if I should split this into multiple contexts?"
Using React DevTools
The React DevTools browser extension has a "Components" tab that shows the component tree, including which Providers exist and what values they hold. Click any component using useContext to see its context value in the hooks section. This is the fastest way to verify a Provider is present, check what it's broadcasting, and confirm values update when expected. For more debugging strategies, see our full guide.
What to Learn Next
Context connects to several core React concepts. Here's where to go from here:
- What Is React? — If you're shaky on components and props, start here. Context builds on top of React's component model.
- What Is State Management? — Context is one approach to state management. This article covers the bigger picture: when you need it and what your options are.
- What Is Zustand? — When Context isn't enough, Zustand is the most popular lightweight alternative. Learn when to reach for it.
- How to Debug AI-Generated Code — Context bugs are common in AI output. This guide teaches you a systematic approach to finding and fixing them.
- What Are Closures? — The stale value problem in Context is actually a closure problem. Understanding closures helps you fix it.
Key Insight
React Context isn't complicated — it's just a broadcast system. Create the channel, plug in the microphone (Provider), and any component can tune in (useContext). The hard part isn't understanding Context itself — it's knowing when Context is the right tool versus when you need something more powerful like Zustand.
FAQ
React Context is a built-in feature that lets you share data across your entire component tree without passing props through every level. You create a context with createContext, wrap components in a Provider, and access the data anywhere with the useContext hook. Think of it as a PA system for your app — you broadcast data once and any component that's listening can hear it.
Prop drilling is when you pass data through multiple intermediate components that don't use the data themselves — they just forward it to their children. It's like passing a note through five people to reach someone sitting ten chairs away. It makes code fragile, hard to maintain, and easy to break when you reorganize components. Context eliminates this by letting any component access shared data directly, no matter how deep in the tree it sits.
Use React Context for data that changes rarely — like theme, language, or the currently authenticated user. Use Zustand for data that changes frequently or where many components read and write from the same store, like shopping carts, form state across pages, or real-time data. Zustand lets components subscribe to specific slices of state, avoiding the performance problems Context causes when values update frequently.
If a component calls useContext but there's no matching Provider higher in the component tree, it gets undefined (or whatever default value you set in createContext). This usually causes a "Cannot read properties of undefined" error when your code tries to access a property on that undefined value. The fix is wrapping your app — or the section that needs the data — in the Provider component. This is the most common Context bug in AI-generated code.
Yes, and it's a best practice. Most production React apps have separate contexts — ThemeContext, AuthContext, LanguageContext. Each has its own Provider and useContext call. Nesting multiple Providers in your App component is normal and expected. Keeping contexts separate improves performance because a change in one context won't re-render components consuming a different one.