Lesson 4: useContext and Context API
Learn how to share state across components without prop drilling using React's Context API and useContext hook
Lesson 4: useContext and Context API ๐
Introduction
Imagine you're building a large office building ๐ข. You need to deliver electricity to every room, but instead of running individual cables from the power plant to each room (prop drilling), you install a central electrical grid that any room can tap into. This is exactly what Context API does in React - it creates a "data grid" that any component can access without passing props through every level.
In the previous lessons, you learned about useState for managing local state and useEffect for handling side effects. But what happens when multiple components far apart in your component tree need to share the same data? Passing props through every intermediate component becomes tedious and error-prone - a problem we call prop drilling. The useContext hook solves this elegantly by allowing components to subscribe to shared data from anywhere in the component tree.
๐ก Did you know? Before hooks, Context required using complex render props or higher-order components. The useContext hook made accessing context data as simple as calling a single function!
Understanding the Problem: Prop Drilling ๐ณ๏ธ
Before diving into Context, let's visualize the problem it solves:
App (user data)
|
Dashboard
|
Sidebar
|
UserMenu
|
UserProfile โ Finally uses user data!
Without Context, you'd need to pass the user data through Dashboard, Sidebar, and UserMenu even though none of them actually use it. They're just middlemen! This creates:
- ๐ด Cluttered component props: Components become messier with props they don't care about
- ๐ด Maintenance nightmares: Adding a new prop requires updating every component in the chain
- ๐ด Reduced reusability: Components become tightly coupled to their parent's data structure
Core Concepts: The Context API Trinity ๐บ
The Context API involves three key players working together:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. CREATE CONTEXT โ
โ const MyContext = โ
โ React.createContext(default) โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. PROVIDE CONTEXT โ
โ <MyContext.Provider value={data}> โ
โ <ChildComponents /> โ
โ </MyContext.Provider> โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. CONSUME CONTEXT โ
โ const data = useContext(MyContext) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 1: Creating Context ๐จ
Think of createContext as establishing a radio frequency. You're not broadcasting anything yet, just setting up the channel:
import React from 'react';
const ThemeContext = React.createContext('light');
The argument 'light' is the default value - used only when a component tries to consume context but isn't wrapped in a Provider (usually a development mistake).
Step 2: Providing Context ๐ก
The Provider component broadcasts data to all descendants. It's like the radio tower that transmits on the frequency you established:
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<MainContent />
</ThemeContext.Provider>
);
}
The value prop is what gets shared. Any component inside the Provider can access it, no matter how deeply nested!
Step 3: Consuming Context ๐ป
The useContext hook is your radio receiver - it tunes into the frequency and receives the broadcast:
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>
Click me!
</button>
);
}
No props needed! ThemedButton can be anywhere in the component tree below the Provider.
Detailed Example 1: Theme Switching ๐
Let's build a complete theme-switching system that demonstrates all three steps:
import React, { createContext, useContext, useState } from 'react';
// Step 1: Create the context
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});
// Custom hook for easier consumption
const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// Step 2: Create a Provider wrapper component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = { theme, toggleTheme };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Step 3: Components that consume the context
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
</header>
);
}
function Content() {
const { theme } = useTheme();
return (
<main style={{
background: theme === 'light' ? '#f0f0f0' : '#222',
color: theme === 'light' ? '#000' : '#fff'
}}>
<p>This content adapts to the theme!</p>
</main>
);
}
// App component wraps everything in the Provider
function App() {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
}
Key Insights:
- ๐ฏ Custom Provider Component: Wrapping the Provider in
ThemeProviderkeeps the state logic organized - ๐ฏ Custom Hook:
useTheme()provides better error messages and cleaner consumption - ๐ฏ Value Object: Passing both state and updater functions gives consumers full control
- ๐ฏ No Prop Drilling:
HeaderandContentaccess theme directly, even thoughAppdoesn't pass props
Detailed Example 2: User Authentication Context ๐ค
A real-world pattern you'll use constantly - managing authenticated user data:
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Check for existing session on mount
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
return { success: true };
}
return { success: false, error: 'Invalid credentials' };
};
const logout = () => {
setUser(null);
// Call logout API
fetch('/api/auth/logout', { method: 'POST' });
};
const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Protected route component
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
// Component using auth
function UserProfile() {
const { user, logout } = useAuth();
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
<button onClick={logout}>Log Out</button>
</div>
);
}
Advanced Patterns Demonstrated:
- โก Loading State: Essential for async operations like checking existing sessions
- โก Multiple Values: Context provides state, actions, and computed values (
isAuthenticated) - โก useEffect Integration: Context Providers can run effects just like regular components
- โก Protected Routes: Context enables powerful patterns like route guards
Detailed Example 3: Multi-Context Application ๐ญ
Real applications often need multiple contexts. Here's how to organize them:
// contexts/ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const useTheme = () => useContext(ThemeContext);
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>
);
}
// contexts/LanguageContext.js
import React, { createContext, useContext, useState } from 'react';
const LanguageContext = createContext();
export const useLanguage = () => useContext(LanguageContext);
const translations = {
en: { welcome: 'Welcome', goodbye: 'Goodbye' },
es: { welcome: 'Bienvenido', goodbye: 'Adiรณs' },
fr: { welcome: 'Bienvenue', goodbye: 'Au revoir' }
};
export function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
const t = (key) => translations[language][key] || key;
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
}
// contexts/index.js - Combine all providers
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import { LanguageProvider } from './LanguageContext';
import { AuthProvider } from './AuthContext';
export function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
{children}
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Usage in App.js
import { AppProviders } from './contexts';
import { useTheme } from './contexts/ThemeContext';
import { useLanguage } from './contexts/LanguageContext';
import { useAuth } from './contexts/AuthContext';
function Greeting() {
const { theme } = useTheme();
const { t } = useLanguage();
const { user } = useAuth();
return (
<h1 style={{ color: theme === 'light' ? '#000' : '#fff' }}>
{t('welcome')}, {user?.name || 'Guest'}!
</h1>
);
}
function App() {
return (
<AppProviders>
<Greeting />
{/* Rest of your app */}
</AppProviders>
);
}
Organization Best Practices:
๐ src/
๐ contexts/
๐ ThemeContext.js โ One context per file
๐ AuthContext.js
๐ LanguageContext.js
๐ index.js โ Combines all providers
๐ components/
๐ App.js
๐ก Pro Tip: Order your providers thoughtfully. Providers that might cause re-renders should be lower in the tree. For example, if AuthProvider updates frequently, place it innermost so theme/language components don't re-render unnecessarily.
Example 4: Context with Reducer Pattern ๐
For complex state logic, combine useContext with useReducer (we'll cover useReducer in the next lesson, but here's a preview):
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
};
export const useCart = () => useContext(CartContext);
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item });
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
const clearCart = () => dispatch({ type: 'CLEAR_CART' });
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const value = {
items: state.items,
addItem,
removeItem,
clearCart,
totalItems,
totalPrice
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
This pattern is powerful for shopping carts, form state, or any complex state management scenario.
Performance Considerations โก
Context is powerful but can cause performance issues if misused. Here's what you need to know:
Problem: Unnecessary Re-renders
When a Context value changes, every component using useContext re-renders, even if they only need part of the data:
// โ PROBLEMATIC: Creates new object every render
function BadProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// This object is recreated on every render!
return (
<MyContext.Provider value={{ user, theme, setUser, setTheme }}>
{children}
</MyContext.Provider>
);
}
Even if only user changes, components that only care about theme will re-render because the value object reference changed.
Solution 1: useMemo for Value Object
import { useMemo } from 'react';
function OptimizedProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// Only creates new object when dependencies change
const value = useMemo(
() => ({ user, theme, setUser, setTheme }),
[user, theme]
);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
Solution 2: Split Contexts
// โ
BETTER: Separate concerns
function Providers({ children }) {
return (
<UserProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</UserProvider>
);
}
// Now components can subscribe to only what they need
function UserProfile() {
const { user } = useUser(); // Won't re-render when theme changes
return <div>{user.name}</div>;
}
function ThemedButton() {
const { theme } = useTheme(); // Won't re-render when user changes
return <button className={theme}>Click</button>;
}
Performance Comparison Table
+------------------------+------------------+-------------------+
| Pattern | Re-renders | Complexity |
+------------------------+------------------+-------------------+
| Single large context | Many (all | Low (simple) |
| | consumers) | |
+------------------------+------------------+-------------------+
| Multiple small | Few (only | Medium (more |
| contexts | relevant ones) | boilerplate) |
+------------------------+------------------+-------------------+
| Context + useMemo | Reduced | Medium (needs |
| | | careful memoization)|
+------------------------+------------------+-------------------+
| State management | Optimized | High (learning |
| library (Redux, etc.) | (selectors) | curve) |
+------------------------+------------------+-------------------+
๐ง Mnemonic: "Split when hot, memo when not" - Split contexts when they update frequently (hot), use useMemo for stable contexts.
Common Mistakes โ ๏ธ
1. Using Context for Everything
// โ WRONG: Local state in global context
function BadApp() {
return (
<FormInputContext.Provider> {/* Why? This is local to one form! */}
<LoginForm />
</FormInputContext.Provider>
);
}
// โ
RIGHT: Keep local state local
function GoodLoginForm() {
const [email, setEmail] = useState(''); // Local state is fine!
const [password, setPassword] = useState('');
const { login } = useAuth(); // Context for shared auth logic
return (
<form onSubmit={() => login(email, password)}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input value={password} onChange={e => setPassword(e.target.value)} />
</form>
);
}
Rule of thumb: Ask "Do 3+ components in different parts of the tree need this?" If no, use local state.
2. Forgetting the Provider
function App() {
return <UserProfile />; {/* โ No Provider! */}
}
function UserProfile() {
const { user } = useAuth(); // This will throw an error or use default value
return <div>{user.name}</div>;
}
Fix: Always wrap your app (or the relevant subtree) in the Provider:
function App() {
return (
<AuthProvider>
<UserProfile />
</AuthProvider>
);
}
3. Creating Context Inside Component
// โ WRONG: Creates new context every render!
function BadComponent() {
const MyContext = createContext();
return <MyContext.Provider value="test">...</MyContext.Provider>;
}
// โ
RIGHT: Create context outside component
const MyContext = createContext();
function GoodComponent() {
return <MyContext.Provider value="test">...</MyContext.Provider>;
}
4. Inline Object Values
// โ WRONG: New object every render
function BadProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}> {/* New object! */}
{children}
</CountContext.Provider>
);
}
// โ
RIGHT: Memoize the value
function GoodProvider({ children }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return (
<CountContext.Provider value={value}>
{children}
</CountContext.Provider>
);
}
5. Not Handling Loading States
// โ WRONG: Assumes data is immediately available
function BadAuthProvider({ children }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser); // Async!
}, []);
return (
<AuthContext.Provider value={{ user }}> {/* user is null during fetch */}
{children}
</AuthContext.Provider>
);
}
// โ
RIGHT: Track loading state
function GoodAuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser()
.then(setUser)
.finally(() => setLoading(false));
}, []);
if (loading) return <LoadingSpinner />;
return (
<AuthContext.Provider value={{ user }}>
{children}
</AuthContext.Provider>
);
}
When NOT to Use Context ๐ซ
Context isn't always the right tool:
โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ USE CONTEXT โ DON'T USE CONTEXT โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โข Theme switching โ โข Form input values โ
โ โข User authentication โ โข Component local state โ
โ โข Language preferences โ โข Highly frequent โ
โ โข Shopping cart โ updates (60fps anims) โ
โ โข Notification system โ โข Server cache data โ
โ โข Feature flags โ (use React Query) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ
Alternatives:
- Prop drilling (2-3 levels): Often clearer than Context for shallow trees
- Composition: Pass JSX as children to avoid drilling
- State management libraries: Redux, Zustand, Jotai for complex apps
- Server state libraries: React Query, SWR for API data
Key Takeaways ๐ฏ
โ Context solves prop drilling by creating shared state accessible anywhere in the component tree
โ Three steps: Create context, wrap in Provider, consume with useContext
โ Provider pattern: Create custom Provider components to encapsulate state logic
โ
Custom hooks: Export custom hooks (like useAuth) for cleaner consumption
โ
Performance matters: Use useMemo for value objects and split frequently-updating contexts
โ Not for everything: Keep local state local, only lift to Context when multiple distant components need it
โ Loading states: Always handle async operations in context providers
โ
Organization: One context per file, combine in an AppProviders component
๐ง Try This!
Before moving to the questions, try building this mini-project:
Shopping Cart Context Challenge ๐
- Create a
CartContextwithitemsarray - Add functions:
addItem,removeItem,updateQuantity - Build a
CartProvidercomponent - Create components:
ProductCardwith "Add to Cart" buttonCartSummaryshowing total items and priceCartDropdowndisplaying all cart items
- Notice how none of these components need props!
๐ Further Study
- React Documentation - useContext: https://react.dev/reference/react/useContext
- React Context Guide: https://react.dev/learn/passing-data-deeply-with-context
- Kent C. Dodds - How to use React Context effectively: https://kentcdodds.com/blog/how-to-use-react-context-effectively
๐ Quick Reference Card
// CREATE CONTEXT
const MyContext = createContext(defaultValue);
// PROVIDER PATTERN
function MyProvider({ children }) {
const [state, setState] = useState(initial);
const value = useMemo(
() => ({ state, setState }),
[state]
);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
// CONSUME CONTEXT
function MyComponent() {
const { state, setState } = useContext(MyContext);
return <div>{state}</div>;
}
// CUSTOM HOOK (Best Practice)
export const useMyContext = () => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within MyProvider');
}
return context;
};
Performance Checklist:
- Value object wrapped in
useMemo - Separate contexts for different concerns
- Loading states for async operations
- Error boundaries around providers
- Custom hooks for consumption
When to use Context:
- โ Theme, language, auth (rarely change)
- โ User preferences, feature flags
- โ Data needed by 3+ distant components
- โ High-frequency updates (animations)
- โ Component-local state
- โ Form field values