The Context API provides a way to share values between components without explicitly passing props.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
// Without Context (prop drilling)
function App() {
const [theme, setTheme] = useState('light');
return (
<Header theme={theme} />
);
}
function Header({ theme }) {
return (
<nav>
<Logo />
<UserMenu theme={theme} />
</nav>
);
}
function UserMenu({ theme }) {
return (
<div className={theme === 'dark' ? 'dark-menu' : 'light-menu'}>
{/* Menu content */}
</div>
);
}You can create a context using React.createContext() and provide a default value.
import { createContext } from 'react';
// Create a context with a default value
const ThemeContext = createContext('light');
// You can also create a context with a more complex default value
const UserContext = createContext({
name: '',
isAuthenticated: false,
preferences: {}
});The Provider component allows consuming components to subscribe to context changes.
import { createContext, useState } from 'react';
// Create the context
const ThemeContext = createContext();
// Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
// The value prop will be available to any component that consumes this context
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Wrap your app with the provider
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
<Footer />
</ThemeProvider>
);
}There are several ways to consume context in your components.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
// Method 1: Using the useContext hook (recommended)
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
className={theme === 'dark' ? 'dark-btn' : 'light-btn'}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}
// Method 2: Using Context.Consumer
function ThemedHeader() {
return (
<ThemeContext.Consumer>
{({ theme }) => (
<header className={theme === 'dark' ? 'dark-header' : 'light-header'}>
<h1>My App</h1>
</header>
)}
</ThemeContext.Consumer>
);
}
// Method 3: Static contextType (only for class components)
class ThemedFooter extends React.Component {
static contextType = ThemeContext;
render() {
const { theme } = this.context;
return (
<footer className={theme === 'dark' ? 'dark-footer' : 'light-footer'}>
© 2023 My App
</footer>
);
}
}You can use multiple contexts in your application by nesting providers.
import { createContext, useState, useContext } from 'react';
// Create contexts
const ThemeContext = createContext();
const UserContext = createContext();
// Create providers
function AppProviders({ children }) {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// Custom hooks to use contexts
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// Usage in components
function ProfilePage() {
const { theme } = useTheme();
const { user } = useUser();
if (!user) {
return <p>Please log in</p>;
}
return (
<div className={theme === 'dark' ? 'dark-profile' : 'light-profile'}>
<h2>{user.name}'s Profile</h2>
{/* Profile content */}
</div>
);
}Combining context with useReducer for more complex state management.
import { createContext, useReducer, useContext } from 'react';
// Define initial state
const initialState = {
todos: [],
loading: false,
error: null
};
// Create reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'SET_LOADING':
return {
...state,
loading: action.payload
};
case 'SET_ERROR':
return {
...state,
error: action.payload
};
default:
return state;
}
}
// Create context
const TodoContext = createContext();
// Create provider
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
// Actions
const addTodo = (todo) => {
dispatch({ type: 'ADD_TODO', payload: todo });
};
const removeTodo = (id) => {
dispatch({ type: 'REMOVE_TODO', payload: id });
};
const fetchTodos = async () => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await fetch('https://api.example.com/todos');
const data = await response.json();
data.forEach(todo => {
addTodo(todo);
});
dispatch({ type: 'SET_ERROR', payload: null });
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
return (
<TodoContext.Provider value={{
todos: state.todos,
loading: state.loading,
error: state.error,
addTodo,
removeTodo,
fetchTodos
}}>
{children}
</TodoContext.Provider>
);
}
// Custom hook to use the context
function useTodos() {
const context = useContext(TodoContext);
if (context === undefined) {
throw new Error('useTodos must be used within a TodoProvider');
}
return context;
}
// Usage in components
function TodoList() {
const { todos, loading, error, fetchTodos, removeTodo } = useTodos();
useEffect(() => {
fetchTodos();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}