On this page
Context and useReducer
React Context
Pass data through the component tree without prop drilling:
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
Usage:
// App.jsx
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
// Any nested component
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>Toggle {theme}</button>
</header>
);
}
Auth Context Example
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth().then(setUser).finally(() => setLoading(false));
}, []);
const login = async (credentials) => {
const user = await api.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
useReducer for Complex State
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
};
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD', text: 'New task' })}>
Add
</button>
{todos.map(todo => (
<div key={todo.id}>
<span onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
{todo.done ? '✓' : '○'} {todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>×</button>
</div>
))}
</div>
);
}
Context + useReducer Pattern
Combine for global state management (similar to Redux):
const AppContext = createContext(null);
const appReducer = (state, action) => {
switch (action.type) {
case 'SET_USER': return { ...state, user: action.payload };
case 'SET_THEME': return { ...state, theme: action.payload };
default: return state;
}
};
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, { user: null, theme: 'light' });
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
When to Use What
| Solution | Use when |
|---|---|
useState |
Local component state |
| Lifting state | Sibling communication |
| Context | Theme, auth, locale — moderate global state |
| useReducer | Complex state transitions |
| Zustand/Redux | Large apps, many state slices |
Context is great for infrequently changing global data — avoid putting fast-changing state in Context (causes wide re-renders).