Context API Deep Dive: Create and Consume Context
The Context API is React's built-in mechanism for sharing state across deep component hierarchies without prop drilling. This practical guide walks through creating a provider component, using the useContext hook to consume context, and building a real-world theme switcher. You will learn how to structure context for global concerns like theming, user authentication, or language preferences.
Key Takeaways
- The Context API allows you to provide global state to any descendant component without passing props down every level
- Create context with
createContext(), then wrap a provider component around children usingContext.Provider - Consume context in any descendant component with the
useContexthook - A context provider can pass both state values and update functions, enabling nested components to trigger state changes
- Theme switchers are a canonical use case for context, allowing any component to access and change the global theme
How Do You Create a Context Provider?
A Context Provider is a component that manages state and makes it available to all descendants. Here is a theme provider that manages light/dark mode:
import React, { useState, createContext } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Breakdown:
createContext()— Creates a new Context object (exported so other components can subscribe to it).useState('light')— Manages the theme state in the provider.toggleTheme— Function to update the theme; passed in the context value.ThemeContext.Provider— Component that provides the context value to all descendants. Only descendants of the provider can access this context.
How Do You Consume Context with useContext?
Any descendant component can access the context value using the useContext hook:
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';
function ThemeSwitcher() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
export default ThemeSwitcher;
Breakdown:
useContext(ThemeContext)— Hook that returns the value object provided by the nearestThemeContext.Providerancestor.- Destructure
themeandtoggleThemefrom the returned object. - Call
toggleThemeto update the theme from any nested component.
How Do You Build a Complete Theme Switcher Application?
Here is a full example that applies the theme to the entire app:
Step 1: Create the ThemeProvider (ThemeProvider.js)
import React, { useState, createContext } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Step 2: Create a ThemeSwitcher Component (ThemeSwitcher.js)
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';
function ThemeSwitcher() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
export default ThemeSwitcher;
Step 3: Wrap the App with the Provider and Apply Theming (App.js)
import React, { useContext } from 'react';
import { ThemeProvider, ThemeContext } from './ThemeProvider';
import ThemeSwitcher from './ThemeSwitcher';
import './App.css';
function AppContent() {
const { theme } = useContext(ThemeContext);
return (
<div className={`App ${theme}`}>
<h1>Theme Switcher</h1>
<ThemeSwitcher />
</div>
);
}
function App() {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
}
export default App;
Step 4: Add CSS for Theming (App.css)
.App {
padding: 2rem;
transition: background-color 0.3s, color 0.3s;
}
.App.light {
background-color: #ffffff;
color: #333333;
}
.App.dark {
background-color: #333333;
color: #ffffff;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.App.light button {
background-color: #007bff;
color: white;
}
.App.dark button {
background-color: #0056b3;
color: white;
}
Walkthrough:
- The
ThemeProviderwraps the entire app insideApp(), making the theme context available to all descendants. AppContentis a nested component that usesuseContext(ThemeContext)to read the theme and apply it to the rootdivviaclassName.ThemeSwitcheralso consumes the context and provides a button to toggle the theme.- CSS applies different styles based on the
lightordarkclass.
Best Practices for Context API
- One provider per concern: Create separate contexts for separate concerns (theme, auth, user preferences). Do not put everything in one monolithic context.
- Memoize provider values: If the provider value is computed on every render, wrap it in
useMemoto prevent unnecessary re-renders of all consumers. - Create custom hooks: Export a custom hook like
useTheme()that wrapsuseContext(ThemeContext)for cleaner consumption. - Use context for global, infrequently-changing state: Context works well for themes, auth, language settings. For high-frequency updates (form input, animations), consider local state.
- Avoid deeply nesting providers: If you have many context providers, consider extracting them into a single provider component to avoid "provider hell."
Frequently Asked Questions
What is the difference between Context and state management libraries like Redux?
Context is simpler and built into React; no external library needed. It works well for global, infrequently-changing state (theme, auth). Redux is more powerful for complex state, time-travel debugging, and high-frequency updates. For most apps, Context is sufficient; reach for Redux if Context becomes unwieldy.
Can a context value change after the Provider is created?
Yes. When state inside the Provider changes (via setTheme), the context value updates, and all consuming components re-render. The Provider itself does not need to be recreated.
What if a component tries to use useContext without a matching Provider ancestor?
React throws an error. You can provide a default value to createContext(defaultValue), but the default is used only if there is no Provider ancestor. Best practice: always wrap your app with the provider.
How do you prevent unnecessary re-renders of context consumers?
Wrap the provider's value object in useMemo to stabilize its reference:
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
Alternatively, split context into separate contexts for state and dispatch to minimize re-render scope.
Can you nest multiple providers of the same context?
Yes. The innermost Provider takes precedence. Nested providers are useful for overriding context at different component tree levels (e.g., a modal with its own theme override).