Table of Contents
Understanding React Context API
The Context API provides a way to pass data through the component tree without having to pass props down manually at every level.
When to Use Context
- Theme data (light/dark mode)
- User authentication state
- Language/locale preferences
- Shopping cart data
Creating a Context
// contexts/ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
{children}
);
}
Using Context in Components
// App.js
import React from 'react';
import { ThemeProvider } from './contexts/ThemeContext';
import Header from './components/Header';
import MainContent from './components/MainContent';
function App() {
return (
);
}
// components/Header.js
import React from 'react';
import { useTheme } from '../contexts/ThemeContext';
function Header() {
const { theme, toggleTheme } = useTheme();
return (
My App
);
}
Complex State Management with useReducer
// contexts/CartContext.js
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 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case 'CLEAR_CART':
return {
...state,
items: []
};
default:
return state;
}
};
const initialState = {
items: [],
total: 0
};
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const value = {
items: state.items,
total,
addItem,
removeItem,
updateQuantity,
clearCart
};
return (
{children}
);
}
Multiple Contexts
// App.js with multiple providers
function App() {
return (
} />
} />
);
}
Practical Exercise
Create a user authentication context that manages:
- User login/logout state
- User profile information
- Authentication tokens
- Loading states
Best Practices
- Don't overuse Context - not every state needs to be global
- Split contexts by concern (theme, auth, cart, etc.)
- Use custom hooks to consume context
- Consider performance implications of context updates