You are viewing a preview of this course. Sign in to start learning

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 ThemeProvider keeps 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: Header and Content access theme directly, even though App doesn'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 ๐Ÿ›’

  1. Create a CartContext with items array
  2. Add functions: addItem, removeItem, updateQuantity
  3. Build a CartProvider component
  4. Create components:
    • ProductCard with "Add to Cart" button
    • CartSummary showing total items and price
    • CartDropdown displaying all cart items
  5. Notice how none of these components need props!

๐Ÿ“š Further Study


๐Ÿ“‹ 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