Learn how to share state between components globally using useContext, eliminating the need for prop drilling.
Imagine you're building a house, and every room needs electricity. You could run extension cords from room to room to room, creating a tangled mess. Or you could install a proper electrical system that makes power available everywhere it's needed.
That's exactly what useContext does for your React app! Instead of passing data through props from component to component (like extension cords), useContext creates a "power grid" that makes data available to any component that needs it.
Let's start by understanding the problem useContext solves. Imagine you're building an app where many components need to know the current user's information:
function App() {
const [user, setUser] = useState({ name: 'Alice', theme: 'dark' });
return (
<div>
<Header user={user} />
<MainContent user={user} />
<Footer user={user} />
</div>
);
}
function Header({ user }) {
return (
<div>
<Navigation user={user} />
<UserMenu user={user} />
</div>
);
}
function Navigation({ user }) {
return (
<nav>
<WelcomeMessage user={user} />
<ThemeToggle user={user} />
</nav>
);
}
function WelcomeMessage({ user }) {
return <span>Welcome, {user.name}!</span>; // Finally using it!
}
function ThemeToggle({ user }) {
return (
<button className={user.theme}>
Switch to {user.theme === 'dark' ? 'light' : 'dark'} theme
</button>
);
}
See the problem? We're passing "user" through 4 levels of components just to get it to where it's actually needed! This is called "prop drilling" and it's a nightmare to maintain.
What happens when:
Your props become a tangled mess! ๐
useContext lets you create a "global state" that any component can access directly, without prop drilling. Here's how it works:
import React, { createContext, useContext, useState } from 'react';
// Create a context
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({
name: 'Alice',
theme: 'dark',
email: 'alice@example.com'
});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function WelcomeMessage() {
const { user } = useContext(UserContext);
return <span>Welcome, {user.name}!</span>;
}
function ThemeToggle() {
const { user, setUser } = useContext(UserContext);
const toggleTheme = () => {
setUser(prev => ({
...prev,
theme: prev.theme === 'dark' ? 'light' : 'dark'
}));
};
return (
<button onClick={toggleTheme} className={user.theme}>
Switch to {user.theme === 'dark' ? 'light' : 'dark'} theme
</button>
);
}
function App() {
return (
<UserProvider>
<div>
<Header /> {/* No props needed! */}
<MainContent /> {/* No props needed! */}
<Footer /> {/* No props needed! */}
</div>
</UserProvider>
);
}
Amazing! No more prop drilling! Any component inside "UserProvider" can access user data directly.
Let's create a real-world example - a theme system that lets users switch between light and dark modes:
import React, { createContext, useContext, useState } from 'react';
// 1. Create the Theme Context
const ThemeContext = createContext();
// 2. Create a custom hook for easier usage
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// 3. Create the Theme Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const themeStyles = {
light: {
backgroundColor: '#ffffff',
color: '#333333',
border: '1px solid #dddddd'
},
dark: {
backgroundColor: '#333333',
color: '#ffffff',
border: '1px solid #555555'
}
};
return (
<ThemeContext.Provider value={{
theme,
toggleTheme,
styles: themeStyles[theme]
}}>
{children}
</ThemeContext.Provider>
);
}
// 4. Components that use the theme
function Header() {
const { styles } = useTheme();
return (
<header style={{
...styles,
padding: '20px',
textAlign: 'center',
borderBottom: styles.border
}}>
<h1>My Awesome App</h1>
<ThemeToggleButton />
</header>
);
}
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
padding: '10px 20px',
backgroundColor: theme === 'light' ? '#007bff' : '#ffc107',
color: theme === 'light' ? 'white' : 'black',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
function MainContent() {
const { styles, theme } = useTheme();
return (
<main style={{
...styles,
padding: '40px',
minHeight: '400px'
}}>
<h2>Welcome to the {theme} theme!</h2>
<p>This content automatically adapts to the current theme.</p>
<div style={{ marginTop: '20px' }}>
<Card title="Example Card">
<p>This card also uses the theme context!</p>
</Card>
</div>
</main>
);
}
function Card({ title, children }) {
const { styles } = useTheme();
return (
<div style={{
...styles,
padding: '20px',
borderRadius: '8px',
margin: '10px 0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3 style={{ marginTop: 0 }}>{title}</h3>
{children}
</div>
);
}
function Footer() {
const { styles } = useTheme();
return (
<footer style={{
...styles,
padding: '20px',
textAlign: 'center',
borderTop: styles.border
}}>
<p>© 2024 My Awesome App. Built with React Context!</p>
</footer>
);
}
// 5. The main App component
function App() {
return (
<ThemeProvider>
<div>
<Header />
<MainContent />
<Footer />
</div>
</ThemeProvider>
);
}
What makes this example great?
You can use multiple contexts in the same app. Let's add a shopping cart context to our theme example:
// Shopping Cart Context
const CartContext = createContext();
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (product) => {
setItems(prev => {
const existingItem = prev.find(item => item.id === product.id);
if (existingItem) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const removeItem = (productId) => {
setItems(prev => prev.filter(item => item.id !== productId));
};
const getTotalItems = () => {
return items.reduce((total, item) => total + item.quantity, 0);
};
const getTotalPrice = () => {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
return (
<CartContext.Provider value={{
items,
addItem,
removeItem,
getTotalItems,
getTotalPrice
}}>
{children}
</CartContext.Provider>
);
}
// Header with cart info
function HeaderWithCart() {
const { styles } = useTheme();
const { getTotalItems } = useCart();
return (
<header style={{ ...styles, padding: '20px', display: 'flex', justifyContent: 'space-between' }}>
<h1>Shopping App</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
<CartIcon />
<ThemeToggleButton />
</div>
</header>
);
}
function CartIcon() {
const { getTotalItems } = useCart();
const totalItems = getTotalItems();
return (
<div style={{ position: 'relative' }}>
<span style={{ fontSize: '24px' }}>๐</span>
{totalItems > 0 && (
<span style={{
position: 'absolute',
top: '-5px',
right: '-5px',
backgroundColor: 'red',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px'
}}>
{totalItems}
</span>
)}
</div>
);
}
function ProductList() {
const { addItem } = useCart();
const { styles } = useTheme();
const products = [
{ id: 1, name: 'Cool T-Shirt', price: 19.99 },
{ id: 2, name: 'Nice Shoes', price: 89.99 },
{ id: 3, name: 'Awesome Hat', price: 24.99 }
];
return (
<div style={{ ...styles, padding: '20px' }}>
<h2>Products</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px' }}>
{products.map(product => (
<div key={product.id} style={{
...styles,
padding: '15px',
borderRadius: '8px',
textAlign: 'center'
}}>
<h3>{product.name}</h3>
<p>$" + product.price + "</p>
<button
onClick={() => addItem(product)}
style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Add to Cart
</button>
</div>
))}
</div>
</div>
);
}
// App with multiple providers
function MultiContextApp() {
return (
<ThemeProvider>
<CartProvider>
<div>
<HeaderWithCart />
<ProductList />
</div>
</CartProvider>
</ThemeProvider>
);
}
โ
Many components need the same data (theme, user, language)
โ
Data needs to be accessible deep in the component tree
โ
You're tired of prop drilling
โ
The data doesn't change very frequently
โ
Data is only needed by direct children
โ
You want to keep components reusable
โ
The relationship between components is clear
โ
Data changes frequently (might cause performance issues with context)
// โ Using context directly
function MyComponent() {
const context = useContext(UserContext);
if (!context) {
throw new Error('Must be used within UserProvider');
}
// ... rest of component
}
// โ
Custom hook
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
function MyComponent() {
const { user, setUser } = useUser(); // Much cleaner!
// ... rest of component
}
// โ One massive context
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
const [notifications, setNotifications] = useState([]);
// ... and 20 more pieces of state
}
// โ
Separate concerns
function UserProvider({ children }) { /* user logic */ }
function ThemeProvider({ children }) { /* theme logic */ }
function CartProvider({ children }) { /* cart logic */ }
function NotificationProvider({ children }) { /* notification logic */ }
// โ Single context with mixed concerns
const AppContext = createContext();
function AppProvider({ children }) {
const [fastChangingData, setFastChangingData] = useState(0);
const [slowChangingData, setSlowChangingData] = useState('stable');
return (
<AppContext.Provider value={{
fastChangingData, setFastChangingData,
slowChangingData, setSlowChangingData
}}>
{children}
</AppContext.Provider>
);
}
// โ
Split by update frequency
const FastDataContext = createContext();
const SlowDataContext = createContext();
function FastDataProvider({ children }) {
const [data, setData] = useState(0);
return (
<FastDataContext.Provider value={{ data, setData }}>
{children}
</FastDataContext.Provider>
);
}
function SlowDataProvider({ children }) {
const [data, setData] = useState('stable');
return (
<SlowDataContext.Provider value={{ data, setData }}>
{children}
</SlowDataContext.Provider>
);
}
Try building a multi-language app with useContext:
Requirements:
Starter code:
const translations = {
en: {
welcome: 'Welcome',
hello: 'Hello',
goodbye: 'Goodbye',
language: 'Language',
switchTo: 'Switch to'
},
es: {
welcome: 'Bienvenido',
hello: 'Hola',
goodbye: 'Adiรณs',
language: 'Idioma',
switchTo: 'Cambiar a'
}
};
// Your context code here!
function LanguageApp() {
return (
// Your provider here
<div>
<Header />
<MainContent />
<LanguageSwitcher />
</div>
// Close provider
);
}
// โ Don't put everything in context
const MegaContext = createContext();
function MegaProvider({ children }) {
const [count, setCount] = useState(0); // Only used in one component
const [tempData, setTempData] = useState(''); // Changes frequently
const [theme, setTheme] = useState('light'); // Good candidate for context
return (
<MegaContext.Provider value={{ /* everything */ }}>
{children}
</MegaContext.Provider>
);
}
// โ
Only put truly global state in context
const ThemeContext = createContext();
// Use regular state for component-specific data
// โ No default value
const UserContext = createContext();
// โ
Provide sensible defaults
const UserContext = createContext({
user: null,
setUser: () => {},
loading: true
});
// โ Creating new objects every render
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{
user,
setUser,
helpers: { // New object every render!
isLoggedIn: !!user,
userName: user?.name || 'Guest'
}
}}>
{children}
</UserContext.Provider>
);
}
// โ
Memoize expensive calculations
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const contextValue = useMemo(() => ({
user,
setUser,
helpers: {
isLoggedIn: !!user,
userName: user?.name || 'Guest'
}
}), [user]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}
Congratulations! You now understand:
โ
What prop drilling is and why it's problematic
โ
How to create and use React Context
โ
When to use Context vs regular props
โ
How to build custom hooks for Context
โ
Best practices for organizing multiple contexts
โ
How to avoid common Context pitfalls
Test your Context knowledge:
Answers: 1) Eliminates prop drilling, 2) Create context, provide it, consume it, 3) For frequently changing data or simple parent-child communication, 4) Better error handling and cleaner API
In our next lesson, we'll learn about useReducer - React's powerful hook for managing complex state logic. When useState becomes too simple and you need more sophisticated state management, useReducer is your next step up!
useContext handles sharing state, and useReducer handles complex state logic!