React Context API: Share State Without Prop Drilling
The Context API is React's built-in solution for sharing state between distant components without passing props through every intermediate layer. Using createContext, Provider, and useContext, you can provide data to any component in a subtree with a single declaration, eliminating prop drilling and reducing component coupling.
Key Takeaways
- Context API Solves Prop Drilling: Share data across the component tree without intermediate components needing to know about it.
- Three Core Functions:
createContext(define context),Provider(supply values),useContext(consume values). - Provider Scope: Only components inside a Provider's subtree can access its context; children outside cannot.
- Context Object Shape: Define what your context provides (string, object, function, array) and pass it as the Provider's
valueprop. - Default Values: Context accepts a default value used when a component calls
useContextoutside a Provider (useful for testing).
Prerequisites
Before reading this article, ensure you understand:
- React Components: How components render and pass props between parent and child.
- React Hooks: Basic familiarity with hooks like
useStateanduseEffect. - Prop Drilling: The problem of passing props through multiple levels of components (covered in earlier articles).
Why Use Context API?
As applications grow, you often need to share state between components far apart in the component tree. The conventional solution is prop drilling: passing props down through multiple intermediate components that don't use them.
Prop Drilling Example:
// State in App
function App() {
const [user, setUser] = useState({ name: 'Alice' });
return <PageLayout user={user} />;
}
// Intermediate layer 1: doesn't use user, just forwards it
function PageLayout({ user }) {
return <Header user={user} />;
}
// Intermediate layer 2: doesn't use user, just forwards it
function Sidebar({ user }) {
return <UserCard user={user} />;
}
// Finally: the component that needs user
function UserCard({ user }) {
return <div>Welcome, {user.name}!</div>;
}
This becomes unwieldy when drilling through 3+ levels or when multiple pieces of state are passed down. The Context API provides a cleaner approach: define a context, provide its value at a high level, and consume it in deeply nested components without intermediate props.
The Three Core Concepts
1. createContext: Define Your Context
createContext() creates a context object that serves as a "channel" for sharing data. You typically store the result in a variable and export it so other components can access it.
// ThemeContext.js
import { createContext } from 'react';
const ThemeContext = createContext('light');
export default ThemeContext;
Parameters:
- The argument to
createContextis the default value used when a component callsuseContextoutside a Provider. This is useful for testing but often never used in production if every context consumer is inside a Provider.
2. Provider: Supply Values to the Context
Every context object includes a .Provider component. You wrap a part of your component tree with it and pass a value prop containing the data you want to share.
// App.js
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
export default App;
Key Points:
- Only components inside the Provider can access the context value.
- You can have multiple Providers in an app, each providing different values.
- The
valueprop can be anything: a string, object, function, or array.
3. useContext: Consume Values in Components
The useContext hook allows any component inside a Provider to read the context value.
// ThemedButton.js
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? '#FFF' : '#333',
}}
>
I am a {theme} button
</button>
);
}
export default ThemedButton;
Complete Example: Theme Switching
Here's a full example showing how context, Provider, and useContext work together:
Step 1: Create the Context
// ThemeContext.js
import { createContext } from 'react';
const ThemeContext = createContext('light');
export default ThemeContext;
Step 2: Provide the Value
// App.js
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
import Header from './Header';
import Content from './Content';
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Header />
<Content />
<button onClick={toggleTheme}>Toggle Theme</button>
</ThemeContext.Provider>
);
}
export default App;
Note: The value is an object { theme, toggleTheme } so consumers can both read the theme and update it.
Step 3: Consume the Context
// Header.js
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Header() {
const { theme } = useContext(ThemeContext);
return (
<header style={{ background: theme === 'dark' ? '#333' : '#FFF' }}>
<h1>App Header (Theme: {theme})</h1>
</header>
);
}
export default Header;
// Content.js (another consumer at a different nesting level)
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Content() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<main style={{ background: theme === 'dark' ? '#222' : '#FFF' }}>
<p>Content area (Theme: {theme})</p>
<button onClick={toggleTheme}>Change Theme</button>
</main>
);
}
export default Content;
Both Header and Content can access the context value and call toggleTheme without any intermediate components needing to forward these props.
Best Practices for Context
1. Split Context by Concern
Don't create a single "everything" context. Instead, split your state logically:
// Good: separate contexts for different concerns
const ThemeContext = createContext();
const AuthContext = createContext();
const LanguageContext = createContext();
// Less ideal: one context for everything
const AppContext = createContext({
theme: 'light',
user: null,
language: 'en',
});
2. Create Custom Hooks for Clarity
Wrap useContext in custom hooks to make usage clearer:
// useTheme.js
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used inside a ThemeProvider');
}
return context;
}
export default useTheme;
Then use it like:
// ThemedButton.js
import useTheme from './useTheme';
function ThemedButton() {
const { theme } = useTheme();
return <button>{theme}</button>;
}
3. Memoize Context Values
When the Provider's value is a new object on every render, all consumers re-render. Use useMemo to prevent this:
import React, { useState, useMemo } from 'react';
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
// Memoize to prevent unnecessary re-renders of consumers
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{/* ... */}
</ThemeContext.Provider>
);
}
Frequently Asked Questions
Can I Update Context Values from a Consuming Component?
Yes, include a setter function in the context value. In the example above, we passed toggleTheme function, which consumers can call to update the theme. This is how you implement editable context.
What Happens if I Call useContext Outside a Provider?
The hook returns the default value passed to createContext(). If no default was provided (or it's undefined), you get undefined. It's best practice to throw an error in a custom hook wrapper to alert developers to this mistake.
Can Multiple Providers Coexist?
Yes, you can nest providers:
<ThemeContext.Provider value="dark">
<AuthContext.Provider value={user}>
<App />
</AuthContext.Provider>
</ThemeContext.Provider>
A component can consume multiple contexts by calling useContext multiple times.
Does Context Replace Redux or State Management Libraries?
Context is useful for moderate state-sharing needs (themes, auth, language). For complex state with many updates, Redux, Zustand, or other libraries may be better. Context isn't a replacement; it's a tool for simpler cases.
How Do I Debug Context Changes?
Use React DevTools' profiler to see which components re-render. Since context consumers re-render when the context value changes, you can identify performance issues. You can also log in your Provider component to track state changes.
Does Context Work with TypeScript?
Yes. Define the shape with TypeScript types:
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);