Master React's Context API to share data across your component tree without prop drilling. Learn createContext, Provider, useContext, and when to use context for themes, auth, and locale.
In React, data flows downward from parent components to child components via props. This is a great design for simple component trees. But what happens when a piece of data needs to be accessed by a deeply nested component?
Imagine a theme value that is set at the top level but needed by a button five levels deep:
function App() {
const theme = 'dark';
return <Layout theme={theme} />;
}
function Layout({ theme }) {
return <Sidebar theme={theme} />;
}
function Sidebar({ theme }) {
return <Menu theme={theme} />;
}
function Menu({ theme }) {
return <MenuItem theme={theme} />;
}
function MenuItem({ theme }) {
return <button className={theme}>Click me</button>;
}Every intermediate component (Layout, Sidebar, Menu) receives theme only to pass it further down. They do not use it themselves. This pattern is called prop drilling, and it creates several problems:
React's Context API solves this by allowing you to share values across the component tree without passing props through every level.
The Context API has three steps: create a context, provide a value, and consume the value.
const ThemeContext = React.createContext('light');createContext takes a default value as its argument. This default is used only when a component reads the context but there is no matching Provider above it in the tree. Think of it as a fallback.
Wrap a portion of your component tree with the context's Provider component and pass it a value prop:
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}Every component inside <ThemeContext.Provider> — no matter how deeply nested — can access the value "dark". The value prop on the Provider overrides the default value passed to createContext.
The Provider value can be state, making the context reactive:
function App() {
const [theme, setTheme] = React.useState('light');
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<Layout />
</ThemeContext.Provider>
);
}When theme changes, React re-renders all components that consume ThemeContext.
The useContext hook reads the current value from the nearest Provider above the component in the tree:
function ThemedButton() {
const theme = React.useContext(ThemeContext);
return <button className={theme}>I am {theme}!</button>;
}No matter how deep ThemedButton is in the component tree, it can read the theme directly — no prop drilling required.
When a component calls useContext(ThemeContext), React walks up the component tree looking for the nearest <ThemeContext.Provider>. It uses that Provider's value. If no Provider is found, it uses the default value from createContext.
// Outer Provider: theme = 'dark'
<ThemeContext.Provider value="dark">
<Navbar /> {/* reads 'dark' */}
{/* Inner Provider: theme = 'light' */}
<ThemeContext.Provider value="light">
<Sidebar /> {/* reads 'light' — nearest Provider wins */}
</ThemeContext.Provider>
</ThemeContext.Provider>It is common to wrap useContext in a custom hook for better ergonomics and error handling:
const ThemeContext = React.createContext(undefined);
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = React.useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage in any component:
function Header() {
const { theme, setTheme } = useTheme();
return <h1 className={theme}>My App</h1>;
}This pattern gives you type safety and a helpful error message if a component tries to use the context outside its Provider.
Context is ideal for data that is global or semi-global within a part of your app. Common use cases:
Context is not a replacement for all props. Avoid it when:
It is perfectly fine (and recommended) to split your context by domain rather than having one giant context:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<Layout />
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}This keeps contexts focused and prevents unnecessary re-renders. When the theme changes, only components that consume ThemeContext re-render — components that only consume AuthContext are unaffected.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const ThemeContext = React.createContext("light");
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState("light");
const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedBox() {
const { theme, toggle } = React.useContext(ThemeContext);
const style = {
background: theme === "dark" ? "#333" : "#eee",
color: theme === "dark" ? "#fff" : "#000",
padding: "20px",
borderRadius: "8px",
};
return (
<div style={style}>
<p>Current theme: {theme}</p>
<button onClick={toggle}>Toggle Theme</button>
</div>
);
}
function App() {
return (
<ThemeProvider>
<h2>Context API Demo</h2>
<ThemedBox />
</ThemeProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>What happens when a component calls useContext but there is no matching Provider above it in the tree?