Back to Tutorial
Advanced Hooks
Hands-on Practiceโ€ข
30 min

useContext Hook

Learn how to share state between components globally using useContext, eliminating the need for prop drilling.

Learning Objectives

  • Understand the problem of prop drilling
  • Learn what React Context is and when to use it
  • Create and use Context Providers and Consumers
  • Master the useContext hook for cleaner code
  • Build a complete theme system with Context
  • Understand Context best practices and limitations

useContext Hook

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.

The Problem: Prop Drilling Hell

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:

  • You add more components that need user data?
  • You want to add more user properties?
  • You need to update user data from a deeply nested component?

Your props become a tangled mess! ๐Ÿ

useContext to the Rescue!

useContext lets you create a "global state" that any component can access directly, without prop drilling. Here's how it works:

Step 1: Create a Context

import React, { createContext, useContext, useState } from 'react'; // Create a context const UserContext = createContext();

Step 2: Provide the Context

function UserProvider({ children }) { const [user, setUser] = useState({ name: 'Alice', theme: 'dark', email: 'alice@example.com' }); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); }

Step 3: Use the Context

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> ); }

Step 4: Wrap Your App

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 Build a Complete Theme System

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>&copy; 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?

  • โœ… No prop drilling - theme data flows directly to components that need it
  • โœ… Custom hook - "useTheme()" provides a clean API
  • โœ… Error handling - throws error if used outside provider
  • โœ… Complete theming - styles automatically update everywhere
  • โœ… Easy to extend - adding new theme properties is simple

Multiple Contexts: Shopping Cart Example

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> ); }

When to Use Context vs Props

Use Context When:

โœ… 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

Use Props When:

โœ… 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)

Context Best Practices

Practice 1: Create Custom Hooks

// โŒ 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 }

Practice 2: Split Large Contexts

// โŒ 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 */ }

Practice 3: Optimize Context Updates

// โŒ 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> ); }

Practice Exercise: Build a Language Switcher

Try building a multi-language app with useContext:

Requirements:

  1. Support English and Spanish languages
  2. Allow switching between languages
  3. Have translations for common phrases
  4. Store current language in localStorage
  5. Any component should be able to access translations

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 ); }

Common Context Mistakes

Mistake 1: Using Context for Everything

// โŒ 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

Mistake 2: Not Providing Default Values

// โŒ No default value const UserContext = createContext(); // โœ… Provide sensible defaults const UserContext = createContext({ user: null, setUser: () => {}, loading: true });

Mistake 3: Performance Issues

// โŒ 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> ); }

What We've Learned

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

Quick Recap Quiz

Test your Context knowledge:

  1. What problem does useContext solve?
  2. What are the three steps to use Context in React?
  3. When should you NOT use Context?
  4. Why is it recommended to create custom hooks for Context?

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

What's Next?

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!