Table of Contents
- What is Context API?
- When to Use Context API
- Core Concepts of Context API
- Step-by-Step Guide to Using Context API
- Advanced Use Cases
- Performance Considerations
- Context API vs. Other State Management Solutions
- Conclusion
- References
What is Context API?
Context API is a React feature that enables components to share state and functions across the component tree without prop drilling. It creates a “context” (a centralized data store) that can be accessed by any component, regardless of its position in the hierarchy.
Think of Context API as a “global variable” for your React app, but scoped to a specific part of the component tree (or the entire app). It eliminates the need to pass props through intermediate components, simplifying state sharing.
Introduced in React 16.3, Context API is built into React—no additional libraries required. It works seamlessly with functional components and React hooks like useState, useReducer, and useContext.
When to Use Context API
Context API is not a one-size-fits-all solution. Use it when:
- Sharing global state: State that multiple components (across different levels) need access to (e.g., user authentication status, theme settings, or language preferences).
- Avoiding prop drilling: When props would otherwise need to be passed through 3+ levels of components (e.g., from
App→Layout→Sidebar→Menu). - Simple to medium-sized apps: For apps with relatively straightforward state logic (complex state may benefit from tools like Redux).
When NOT to use Context API:
- For local component state: If state is only used within a single component (e.g., a form input’s value), stick to
useState. - For frequently changing state: Context can trigger unnecessary re-renders if overused (see Performance Considerations).
Core Concepts of Context API
To use Context API effectively, you need to understand three core concepts:
1. createContext
Initializes a new context. It takes a default value (used only when a component consumes the context without a matching provider above it in the tree).
import { createContext } from 'react';
// Create a context for theme management
const ThemeContext = createContext('light'); // Default value: 'light'
2. Context.Provider
A component that “provides” the context value to its child components. It accepts a value prop, which can be any data type (primitives, objects, functions, etc.). All components nested inside the provider can access this value.
<ThemeContext.Provider value="dark">
{/* Child components here can access the "dark" theme */}
</ThemeContext.Provider>
3. Consuming Context
Components can “consume” context in two ways:
useContextHook: The modern, hook-based approach (recommended for functional components).Context.Consumer: A render prop component (older approach, useful for class components).
We’ll focus on useContext since hooks are now the standard in React:
import { useContext } from 'react';
function ThemedButton() {
// Access the context value
const theme = useContext(ThemeContext);
return <button style={{ background: theme === 'dark' ? 'black' : 'white' }}>Click me</button>;
}
Step-by-Step Guide to Using Context API
Let’s build a practical example: a theme toggle system that lets users switch between light and dark modes. We’ll use Context API to share the theme state across components.
Step 1: Create the Context
First, define a context to hold the theme state and a function to toggle it.
// src/contexts/ThemeContext.js
import { createContext, useState, useContext } from 'react';
// Initialize context with a default value (optional, but helpful for TypeScript)
const ThemeContext = createContext();
// Custom hook to simplify consuming the context
export function useTheme() {
return useContext(ThemeContext);
}
// Provider component to wrap the app and provide the theme state
export function ThemeProvider({ children }) {
// State to hold the current theme
const [theme, setTheme] = useState('light');
// Function to toggle the theme
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
// Value to pass to context consumers (theme and toggle function)
const value = { theme, toggleTheme };
return (
<ThemeContext.Provider value={value}>
{children} {/* Child components will have access to "value" */}
</ThemeContext.Provider>
);
}
Here, we’ve also created a custom hook useTheme() to make consuming the context cleaner (avoids importing ThemeContext and useContext everywhere).
Step 2: Wrap Your App with the Provider
Next, wrap your root component (e.g., App) with ThemeProvider so all child components can access the theme context.
// src/App.js
import { ThemeProvider } from './contexts/ThemeContext';
import Navbar from './components/Navbar';
import ThemeToggle from './components/ThemeToggle';
function App() {
return (
<ThemeProvider>
<Navbar />
<ThemeToggle />
</ThemeProvider>
);
}
export default App;
Step 3: Consume the Context in Child Components
Example 1: A Themed Navbar
Create a Navbar component that uses the theme to style its background:
// src/components/Navbar.js
import { useTheme } from '../contexts/ThemeContext';
function Navbar() {
const { theme } = useTheme(); // Access theme from context
return (
<nav style={{
background: theme === 'light' ? '#f0f0f0' : '#333',
color: theme === 'light' ? '#333' : 'white',
padding: '1rem'
}}>
<h1>My App</h1>
</nav>
);
}
export default Navbar;
Example 2: A Theme Toggle Button
Create a ThemeToggle component that triggers the toggleTheme function from the context:
// src/components/ThemeToggle.js
import { useTheme } from '../contexts/ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme(); // Access both theme and toggle function
return (
<button
onClick={toggleTheme}
style={{
padding: '0.5rem 1rem',
border: 'none',
borderRadius: '4px',
background: theme === 'light' ? '#333' : '#f0f0f0',
color: theme === 'light' ? 'white' : '#333'
}}
>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
export default ThemeToggle;
Step 4: Test the App
Run your app, and you’ll see:
- The
Navbarbackground color updates based on the theme. - Clicking the
ThemeTogglebutton switches between light and dark modes.
Advanced Use Cases
Context API isn’t limited to simple state. Let’s explore more powerful patterns.
1. Multiple Contexts
For larger apps, split state into multiple contexts to avoid bloating a single context with unrelated data. For example:
ThemeContext: Manages UI theme.UserContext: Manages user authentication state.
// src/contexts/UserContext.js
import { createContext, useState, useContext } from 'react';
const UserContext = createContext();
export function useUser() {
return useContext(UserContext);
}
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
Wrap your app with both providers (order doesn’t matter):
// src/App.js
import { ThemeProvider } from './contexts/ThemeContext';
import { UserProvider } from './contexts/UserContext';
function App() {
return (
<ThemeProvider>
<UserProvider>
{/* App content */}
</UserProvider>
</ThemeProvider>
);
}
2. Using useReducer with Context
For complex state logic (e.g., state with multiple sub-values or interdependent updates), combine useReducer with Context. useReducer simplifies state transitions by centralizing logic in a reducer function.
Example: A todo list context with add/delete functionality:
// src/contexts/TodoContext.js
import { createContext, useReducer, useContext } from 'react';
const TodoContext = createContext();
// Reducer function: handles state updates
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
export function useTodos() {
return useContext(TodoContext);
}
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}
Consume it in a component:
// src/components/TodoList.js
import { useTodos } from '../contexts/TodoContext';
function TodoList() {
const { todos, dispatch } = useTodos();
return (
<div>
<h2>Todos</h2>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
Delete
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'ADD_TODO', payload: 'New todo' })}>
Add Todo
</button>
</div>
);
}
Performance Considerations
A common pitfall with Context API is unnecessary re-renders: when the context value changes, all components consuming that context re-render, even if they don’t use the updated part of the value. Here’s how to optimize:
1. Split Contexts
Avoid putting all state into a single context. Split unrelated state into separate contexts (e.g., ThemeContext and UserContext) so updates to one don’t trigger re-renders for components using the other.
2. Memoize Context Values
If your context value is an object or function, wrap it in useMemo to prevent unnecessary re-renders. For example:
// In ThemeProvider
const value = useMemo(() => ({ theme, toggleTheme }), [theme]); // Only update when "theme" changes
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
3. Memoize Functions with useCallback
Functions passed in context (e.g., toggleTheme) are recreated on every render by default, causing the context value to change (even if the function logic is the same). Use useCallback to memoize functions:
// In ThemeProvider
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []); // Empty dependency array: function never changes
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
4. Use React.memo for Consumer Components
Wrap consumer components with React.memo to prevent re-renders if their props (and context) haven’t changed:
import React from 'react';
const ThemedButton = React.memo(function ThemedButton() {
const { theme } = useTheme();
return <button style={{ background: theme }}>Click me</button>;
});
Context API vs. Other State Management Solutions
| Tool | Use Case | Pros | Cons |
|---|---|---|---|
| Context API | Simple/medium apps, global state | Built into React, no dependencies, simple | Limited features (no middleware/dev tools) |
| Redux | Large apps, complex state | Mature ecosystem, dev tools, middleware | Verbose boilerplate (solved by Redux Toolkit) |
| Redux Toolkit | Large apps, simplified Redux | Less boilerplate, built-in tools | Still more complex than Context for simple use cases |
| Zustand | Lightweight global state | Minimal API, no providers needed | Smaller ecosystem than Redux |
| Recoil/Jotai | Atomized state (fine-grained updates) | Optimized for performance, atomic state | Newer, smaller community |
Verdict: Context API is best for most simple to medium apps. For large apps with complex state logic (e.g., async workflows, middleware), use Redux Toolkit. For lightweight, atomic state, try Zustand or Jotai.
Conclusion
Context API is a powerful, built-in tool for sharing state across React components. It simplifies global state management, eliminates prop drilling, and works seamlessly with hooks like useState and useReducer.
By following best practices—splitting contexts, memoizing values, and optimizing re-renders—you can use Context API to build scalable, maintainable apps. For most projects, it’s the perfect balance of simplicity and functionality.