What is the Context API, and how do you use it to manage state?
The Context API is a built-in feature in React that provides a way to share data between components without having to explicitly pass props through every level of the component tree. It’s designed to solve the problem of “prop drilling” - passing props through intermediate components that don’t need the data but only serve as a means to pass it down.
Core Components of Context API
The Context API consists of three main parts:
- React.createContext: Creates a Context object
- Context.Provider: Provides the context value to components
- Context.Consumer or useContext hook: Consumes the context value
Creating and Using Context
Step 1: Create a Context
import React, { createContext } from 'react';
// Create a context with a default value
const ThemeContext = createContext('light');
// Export the context for use in other components
export default ThemeContext;
Step 2: Provide the Context
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<div className={`app ${theme}`}>
<Header />
<MainContent />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
</ThemeContext.Provider>
);
}
Step 3: Consume the Context
Using useContext Hook (Functional Components)
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemedButton() {
// Get the current theme value from context
const theme = useContext(ThemeContext);
return (
<button className={`button-${theme}`}>
I'm styled based on the theme!
</button>
);
}
Using Context.Consumer (Class or Functional Components)
import React from 'react';
import ThemeContext from './ThemeContext';
function ThemedButton() {
return (
<ThemeContext.Consumer>
{theme => (
<button className={`button-${theme}`}>
I'm styled based on the theme!
</button>
)}
</ThemeContext.Consumer>
);
}
Using contextType (Class Components Only)
import React from 'react';
import ThemeContext from './ThemeContext';
class ThemedButton extends React.Component {
// Set the context type
static contextType = ThemeContext;
render() {
const theme = this.context;
return (
<button className={`button-${theme}`}>
I'm styled based on the theme!
</button>
);
}
}
Managing State with Context
While Context itself is just a mechanism for passing data, it’s commonly used with React’s state management to create a simple state management solution.
Basic State Management Pattern
import React, { createContext, useState, useContext } from 'react';
// Step 1: Create context with a default value
const CounterContext = createContext({
count: 0,
increment: () => {},
decrement: () => {}
});
// Step 2: Create a provider component
export function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
// Create the value object that will be provided
const value = {
count,
increment,
decrement
};
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
);
}
// Step 3: Create a custom hook for consuming the context
export function useCounter() {
const context = useContext(CounterContext);
if (context === undefined) {
throw new Error('useCounter must be used within a CounterProvider');
}
return context;
}
// Step 4: Use the provider at the top level
function App() {
return (
<CounterProvider>
<div className="app">
<Counter />
<OtherComponent />
</div>
</CounterProvider>
);
}
// Step 5: Consume the context in components
function Counter() {
const { count, increment, decrement } = useCounter();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
function OtherComponent() {
const { count } = useCounter();
return (
<div>
<p>The current count is: {count}</p>
</div>
);
}
Using Context with useReducer
For more complex state logic, you can combine Context with useReducer
:
import React, { createContext, useReducer, useContext } from 'react';
// Step 1: Define the initial state
const initialState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null
};
// Step 2: Create a reducer function
function authReducer(state, action) {
switch (action.type) {
case 'LOGIN_REQUEST':
return {
...state,
isLoading: true,
error: null
};
case 'LOGIN_SUCCESS':
return {
...state,
isLoading: false,
isAuthenticated: true,
user: action.payload,
error: null
};
case 'LOGIN_FAILURE':
return {
...state,
isLoading: false,
isAuthenticated: false,
user: null,
error: action.payload
};
case 'LOGOUT':
return {
...state,
isAuthenticated: false,
user: null
};
default:
return state;
}
}
// Step 3: Create the context
const AuthContext = createContext();
// Step 4: Create a provider component
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, initialState);
// Define actions
const login = async (credentials) => {
try {
dispatch({ type: 'LOGIN_REQUEST' });
// Simulate API call
const response = await fakeAuthApi.login(credentials);
dispatch({
type: 'LOGIN_SUCCESS',
payload: response.user
});
} catch (error) {
dispatch({
type: 'LOGIN_FAILURE',
payload: error.message
});
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
// Create value object with state and actions
const value = {
...state,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Step 5: Create a custom hook for consuming the context
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Fake auth API for the example
const fakeAuthApi = {
login: async (credentials) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.username === 'user' && credentials.password === 'pass') {
resolve({
user: {
id: 1,
username: 'user',
name: 'Test User'
}
});
} else {
reject(new Error('Invalid credentials'));
}
}, 1000);
});
}
};
Multiple Contexts
You can use multiple contexts in your application for different concerns:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<Router>
<AppContent />
</Router>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}
Context API Best Practices
1. Split contexts by domain
Create separate contexts for different domains of your application:
// Good: Separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
// Avoid: One giant context
const AppContext = createContext();
2. Provide default values
Always provide meaningful default values for your contexts:
// With default value
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
// Without default value (less ideal)
const ThemeContext = createContext();
3. Create custom hooks
Create custom hooks to consume your contexts:
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
4. Optimize renders
Use React.memo and careful context structure to prevent unnecessary re-renders:
// Split state and dispatch into separate contexts
const CountStateContext = createContext();
const CountDispatchContext = createContext();
function CountProvider({ children }) {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountStateContext.Provider value={count}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
5. Keep provider components close to where they’re needed
Don’t always put all providers at the root level:
function App() {
return (
<AuthProvider>
<Router>
<Route path="/admin">
{/* AdminProvider only used in admin section */}
<AdminProvider>
<AdminDashboard />
</AdminProvider>
</Route>
<Route path="/shop">
{/* CartProvider only used in shop section */}
<CartProvider>
<Shop />
</CartProvider>
</Route>
</Router>
</AuthProvider>
);
}
Context API vs. Other State Management Solutions
Feature | Context API | Redux | MobX |
---|---|---|---|
Learning curve | Low | Medium-High | Medium |
Boilerplate | Minimal | Significant | Minimal |
Debugging tools | Limited | Excellent | Good |
Performance | Good for small-medium apps | Excellent for large apps | Good |
Middleware support | Manual implementation | Built-in | Manual implementation |
Time travel debugging | No | Yes | No |
Best suited for | Small-medium apps, component-specific state | Large apps, complex state logic | Medium-large apps |
Interview Tips
Explain the purpose: Context API solves the problem of prop drilling by providing a way to share values between components without passing props through every level.
Describe the components: Be ready to explain the three main parts: createContext, Provider, and Consumer/useContext.
Compare with prop drilling: Highlight the benefits of Context over passing props through multiple levels of components.
Discuss performance implications: Mention that Context is not optimized for high-frequency updates and may cause re-renders in all consuming components.
Compare with other state management: Be prepared to compare Context with Redux, MobX, or other state management libraries.
Mention best practices: Discuss splitting contexts by domain, providing default values, and creating custom hooks.
Real-world examples: Share examples of how you’ve used Context API in your projects, such as for theme switching, authentication, or shopping carts.
Limitations: Acknowledge that Context is not a complete state management solution and may not be suitable for all use cases, especially those requiring high-performance updates.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.