Welcome to Week 5: Advanced React Concepts!
Today we're diving into one of React's most powerful hooks: useContext. Imagine you're running a pizza restaurant, and you need to share the customer's order details across multiple stations: the order counter, kitchen, and delivery desk. That's exactly what useContext does for our React applications - it shares data across different components without passing props through every level.
The Problem: Prop Drilling
Before we understand the solution, let's look at the problem. Imagine building a theme switcher app:
// Without useContext - Prop Drilling Hell! 😱
function App() {
const [theme, setTheme] = useState('light');
return (
<div>
<Navbar theme={theme} />
<MainContent theme={theme} />
<Footer theme={theme} />
</div>
);
}
function Navbar({ theme }) {
return (
<nav>
<Logo theme={theme} />
<NavLinks theme={theme} />
</nav>
);
}
function NavLinks({ theme }) {
return (
<ul>
<NavLink theme={theme} text="Home" />
<NavLink theme={theme} text="About" />
</ul>
);
}
// And it keeps going deeper... 😵
The Solution: Context API + useContext
Context is like a global warehouse for your data. Any component can access it directly without the data passing through every component in between.
// Step 1: Create the Context
const ThemeContext = React.createContext();
// Step 2: Create a Provider Component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Step 3: Use the Context
function NavLink({ text }) {
const { theme } = useContext(ThemeContext);
return (
<li className={`nav-link ${theme}`}>
{text}
</li>
);
}
// Step 4: Wrap your app
function App() {
return (
<ThemeProvider>
<Navbar />
<MainContent />
<Footer />
</ThemeProvider>
);
}
Real-World Example: Shopping Cart System
Let's build a mini e-commerce application with a global shopping cart:
// 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
)
};
default:
return state;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addToCart = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeFromCart = (productId) => {
dispatch({ type: 'REMOVE_ITEM', payload: productId });
};
const updateQuantity = (productId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: productId, quantity } });
};
const cartTotal = state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
return (
<CartContext.Provider
value={{
items: state.items,
addToCart,
removeFromCart,
updateQuantity,
cartTotal
}}
>
{children}
</CartContext.Provider>
);
}
// Custom hook for easier usage
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
// Product.js
function Product({ product }) {
const { addToCart } = useCart();
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
);
}
// CartIcon.js
function CartIcon() {
const { items } = useCart();
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
return (
<div className="cart-icon">
🛒 {itemCount > 0 && <span className="badge">{itemCount}</span>}
</div>
);
}
Common Context Patterns
Pattern 1: Authentication Context
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in
const token = localStorage.getItem('token');
if (token) {
fetchUser(token).then(userData => {
setUser(userData);
setLoading(false);
});
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
const response = await authAPI.login(email, password);
setUser(response.user);
localStorage.setItem('token', response.token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
if (loading) {
return <LoadingSpinner />;
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Pattern 2: Multi-Language Support
const LanguageContext = createContext();
const translations = {
en: {
welcome: 'Welcome',
goodbye: 'Goodbye',
addToCart: 'Add to Cart'
},
es: {
welcome: 'Bienvenido',
goodbye: 'Adiós',
addToCart: 'Añadir al carrito'
}
};
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>
);
}
// Usage
function WelcomeMessage() {
const { t } = useContext(LanguageContext);
return <h1>{t('welcome')}!</h1>;
}
Best Practices and Gotchas
✅ DO: Create Custom Hooks
// Good practice - encapsulate context logic
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
⚠️ AVOID: Context for Everything
Not everything needs to be in context. Use it for:
- User authentication state
- Theme/appearance preferences
- Language settings
- Shopping cart data
Don't use it for:
- Form input values
- UI state that affects only one component
- Data that's only used in a small part of your app
🎯 Performance Tip: Split Contexts
// Instead of one giant context
const AppContext = createContext({
user: null,
theme: 'light',
cart: [],
notifications: []
});
// Split into focused contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
const NotificationContext = createContext();
Practice Exercises
Exercise 1: Create a Notification System
Build a context that manages toast notifications (success, error, info) that can be triggered from any component.
// Your task: Complete this NotificationProvider
function NotificationProvider({ children }) {
// 1. Create state for notifications array
// 2. Create addNotification function
// 3. Create removeNotification function
// 4. Auto-dismiss notifications after 3 seconds
// 5. Provide value to children
}
Exercise 2: Settings Context
Create a context for user preferences that persists to localStorage:
- Font size (small, medium, large)
- Color scheme (light, dark, high-contrast)
- Animations enabled/disabled
Exercise 3: Form Context
Build a multi-step form with context sharing form data between steps.
Key Takeaways
- useContext eliminates prop drilling
- Always create custom hooks for your contexts
- Split contexts by concern for better performance
- Not everything needs to be in context
- Context is perfect for app-wide state like auth, theme, and language