A practical, code-heavy guide to using React’s Context API for efficient global state management without external libraries.
1. Introduction
React’s Context API is the built-in solution for sharing state across components without prop drilling. Ideal for themes, authentication, language settings, or any global data.
No extra dependencies. Lightweight. Perfect for most medium-sized apps.
Let’s skip the theory and jump straight into code with multiple practical examples.
2. The Problem: Prop Drilling
Without Context, global data forces you to pass props through every level:
// Bad: Prop drilling hell
<App>
<Header user={user} />
<Main>
<Sidebar user={user} />
<Content>
<DeepComponent user={user} /> {/* user passed through 5 levels */}
</Content>
</Main>
</App>Clean code becomes messy fast.
3. The Solution: Context API
Create once, provide at the top, consume anywhere.
Core tools:
• createContext()
• Provider
• useContext() hook
4. Implementation
Example 1: Theme Toggle (Most Common Use Case)
ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);App.js
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Dashboard from './Dashboard';
function App() {
return (
<ThemeProvider>
<div className="app">
<Header />
<Dashboard />
</div>
</ThemeProvider>
);
}
export default App;Header.js (uses context directly)
import { useTheme } from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={`header ${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? 'Dark' : 'Light'}
</button>
</header>
);
}
export default Header;DeepComponent.js (deeply nested – still easy access)
import { useTheme } from './ThemeContext';
function SettingsPanel() {
const { theme } = useTheme();
return (
<div className={`panel ${theme}`}>
<p>Current theme: {theme}</p>
{/* No props needed! */}
</div>
);
}
export default SettingsPanel;Example 2: User Authentication Context
AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
const isAuthenticated = !!user;
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);LoginButton.js
import { useAuth } from './AuthContext';
function LoginButton() {
const { user, login, logout } = useAuth();
if (user) {
return <button onClick={logout}>Logout {user.name}</button>;
}
return <button onClick={() => login({ name: 'John' })}>Login</button>;
}
export default LoginButton;ProtectedComponent.js (deep in the tree)
import { useAuth } from './AuthContext';
function Dashboard() {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) return <p>Please log in</p>;
return <h2>Welcome back, {user.name}!</h2>;
}
export default Dashboard;Example 3: Advanced – Context + useReducer (Redux-like)
CartContext.js
import { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const initialState = { items: [], total: 0 };
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
const filtered = state.items.filter(item => item.id !== action.payload);
const removed = state.items.find(item => item.id === action.payload);
return {
items: filtered,
total: state.total - (removed?.price || 0)
};
case 'CLEAR':
return initialState;
default:
return state;
}
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => useContext(CartContext);AddToCartButton.js
import { useCart } from './CartContext';
function AddToCartButton({ product }) {
const { dispatch } = useCart();
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
Add to Cart
</button>
);
}
export default AddToCartButton;CartSummary.js (anywhere in app)
import { useCart } from './CartContext';
function CartSummary() {
const { state } = useCart();
return (
<div>
<p>Items: {state.items.length}</p>
<p>Total: ${state.total}</p>
</div>
);
}
export default CartSummary;Bonus: Multiple Contexts (Best Practice)
// App.js – Multiple providers
<ThemeProvider>
<AuthProvider>
<CartProvider>
<AppContent />
</CartProvider>
</AuthProvider>
</ThemeProvider>5. Conclusion
Context API eliminates prop drilling with minimal code.
Use it for:
• Themes
• Auth state
• User preferences
• Shopping cart
• Language settings
Combine with useReducer for complex logic.
Custom hooks (useTheme, useAuth) make consumption clean.