Skip to main content

Command Palette

Search for a command to run...

useContext

Published
•8 min read•View as Markdown
useContext

Understanding useContext: React's "Pass Data Without Props" Hook

Hey! Today we'll learn about useContext - it's like creating a global announcement system for your React app where components can listen to important information without passing it through every level.

What Problem Does useContext Solve?

Imagine you're passing a piece of data from a grandparent component to a great-grandchild component:

// ❌ The Prop Drilling Problem
function Grandparent() {
  const theme = 'dark';
  return <Parent theme={theme} />;
}

function Parent({ theme }) {
  return <Child theme={theme} />;
}

function Child({ theme }) {
  return <Grandchild theme={theme} />;
}

function Grandchild({ theme }) {
  return <p>Theme is: {theme}</p>;
}

That's prop drilling - passing props through components that don't need them just to reach a component that does.

useContext fixes this!

The Basic Idea

Think of useContext like a radio broadcast:

  1. Context.Provider = Radio station (broadcasts data)

  2. useContext() = Radio receiver (listens to data)

  3. Any component can tune in to the broadcast, no matter where it is!

Three Simple Steps to Use Context

Step 1: Create the Context (The Radio Station)

import React, { createContext } from 'react';

// Create the radio station
const ThemeContext = createContext('light'); // Default value = 'light'
// This is like setting up a radio frequency

Step 2: Provide the Value (Start Broadcasting)

function App() {
  const [theme, setTheme] = useState('dark');

  // Wrap components that need this data in Provider
  return (
    <ThemeContext.Provider value={theme}>
      <Header />
      <MainContent />
      <Footer />
    </ThemeContext.Provider>
  );
}
// Now everything inside can listen to the theme!

Step 3: Consume the Value (Tune In)

import React, { useContext } from 'react';

function Header() {
  // Tune into the ThemeContext broadcast
  const theme = useContext(ThemeContext);

  return (
    <header className={`header-${theme}`}>
      <h1>My App (Theme: {theme})</h1>
    </header>
  );
}
// No props needed! Header just "knows" the theme

Complete Real Example: Theme Switcher

Let's build a complete theme switcher app:

import React, { createContext, useState, useContext } from 'react';

// 1. CREATE Context (outside components)
const ThemeContext = createContext();
const ThemeUpdateContext = createContext(); // Another context for update function

// Custom hook for easy theme access
function useTheme() {
  return useContext(ThemeContext);
}

// Custom hook for easy theme updates
function useThemeUpdate() {
  return useContext(ThemeUpdateContext);
}

// 2. PROVIDER Component (wraps our app)
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={theme}>
      <ThemeUpdateContext.Provider value={toggleTheme}>
        {children}
      </ThemeUpdateContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. COMPONENTS that use the context
function Header() {
  const theme = useTheme();

  return (
    <header style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff',
      padding: '20px'
    }}>
      <h1>My Website</h1>
      <p>Current theme: {theme}</p>
    </header>
  );
}

function Content() {
  const theme = useTheme();

  return (
    <main style={{
      background: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#000' : '#fff',
      padding: '20px',
      minHeight: '200px'
    }}>
      <p>This is the main content area.</p>
      <p>It automatically uses the {theme} theme!</p>
    </main>
  );
}

function ThemeToggleButton() {
  const toggleTheme = useThemeUpdate();
  const theme = useTheme();

  return (
    <button 
      onClick={toggleTheme}
      style={{
        padding: '10px 20px',
        margin: '10px',
        background: theme === 'light' ? '#333' : '#fff',
        color: theme === 'light' ? '#fff' : '#333'
      }}
    >
      Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

// 4. PUT IT ALL TOGETHER
function App() {
  return (
    <ThemeProvider>
      <div className="App">
        <Header />
        <Content />
        <ThemeToggleButton />
        <Footer />
      </div>
    </ThemeProvider>
  );
}

function Footer() {
  const theme = useTheme();

  return (
    <footer style={{
      background: theme === 'light' ? '#eee' : '#111',
      color: theme === 'light' ? '#000' : '#fff',
      padding: '10px',
      marginTop: '20px'
    }}>
      <p>Footer - Using {theme} theme</p>
    </footer>
  );
}

Multiple Contexts Example: User + Cart

import React, { createContext, useState, useContext } from 'react';

// Context 1: User Information
const UserContext = createContext();
// Context 2: Shopping Cart
const CartContext = createContext();

function App() {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john@example.com',
    isPremium: true
  });

  const [cart, setCart] = useState([
    { id: 1, name: 'iPhone', price: 999 },
    { id: 2, name: 'Headphones', price: 199 }
  ]);

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  return (
    <UserContext.Provider value={user}>
      <CartContext.Provider value={{ cart, addToCart }}>
        <Navbar />
        <Products />
        <Checkout />
      </CartContext.Provider>
    </UserContext.Provider>
  );
}

function Navbar() {
  const user = useContext(UserContext);

  return (
    <nav>
      <p>Welcome, {user.name}!</p>
      <p>{user.isPremium ? '⭐ Premium User' : 'Free User'}</p>
    </nav>
  );
}

function Products() {
  const { addToCart } = useContext(CartContext);

  const products = [
    { id: 3, name: 'Laptop', price: 1299 },
    { id: 4, name: 'Tablet', price: 599 }
  ];

  return (
    <div>
      <h2>Products</h2>
      {products.map(product => (
        <div key={product.id} style={{border: '1px solid #ccc', padding: '10px', margin: '10px'}}>
          <h3>{product.name}</h3>
          <p>${product.price}</p>
          <button onClick={() => addToCart(product)}>
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

function Checkout() {
  const user = useContext(UserContext);
  const { cart } = useContext(CartContext);

  const total = cart.reduce((sum, item) => sum + item.price, 0);

  return (
    <div>
      <h2>Checkout</h2>
      <p>Customer: {user.email}</p>

      <h3>Cart Items:</h3>
      {cart.map(item => (
        <p key={item.id}>{item.name} - ${item.price}</p>
      ))}

      <h3>Total: ${total}</h3>
      {user.isPremium && <p>Premium Discount Applied! Final: ${total * 0.9}</p>}
    </div>
  );
}

Dynamic Context with Updates Example

import React, { createContext, useState, useContext } from 'react';

const SettingsContext = createContext();

function SettingsProvider({ children }) {
  const [settings, setSettings] = useState({
    language: 'English',
    fontSize: 16,
    notifications: true,
    darkMode: false
  });

  const updateSetting = (key, value) => {
    setSettings(prev => ({
      ...prev,
      [key]: value
    }));
  };

  return (
    <SettingsContext.Provider value={{ settings, updateSetting }}>
      {children}
    </SettingsContext.Provider>
  );
}

function SettingsPanel() {
  const { settings, updateSetting } = useContext(SettingsContext);

  return (
    <div style={{border: '2px solid #666', padding: '20px', margin: '20px'}}>
      <h2>Settings Panel</h2>

      <div>
        <label>Language: </label>
        <select 
          value={settings.language}
          onChange={(e) => updateSetting('language', e.target.value)}
        >
          <option>English</option>
          <option>Spanish</option>
          <option>French</option>
        </select>
      </div>

      <div>
        <label>Font Size: {settings.fontSize}px </label>
        <input 
          type="range" 
          min="12" 
          max="24" 
          value={settings.fontSize}
          onChange={(e) => updateSetting('fontSize', parseInt(e.target.value))}
        />
      </div>

      <div>
        <label>
          <input 
            type="checkbox"
            checked={settings.notifications}
            onChange={(e) => updateSetting('notifications', e.target.checked)}
          />
          Enable Notifications
        </label>
      </div>

      <div>
        <label>
          <input 
            type="checkbox"
            checked={settings.darkMode}
            onChange={(e) => updateSetting('darkMode', e.target.checked)}
          />
          Dark Mode
        </label>
      </div>
    </div>
  );
}

function DisplayContent() {
  const { settings } = useContext(SettingsContext);

  return (
    <div style={{
      background: settings.darkMode ? '#333' : '#fff',
      color: settings.darkMode ? '#fff' : '#000',
      fontSize: `${settings.fontSize}px`,
      padding: '20px',
      margin: '20px'
    }}>
      <h1>Content Display</h1>
      <p>Current language: {settings.language}</p>
      <p>Font size: {settings.fontSize}px</p>
      <p>Notifications: {settings.notifications ? 'ON' : 'OFF'}</p>
      <p>This text changes based on your settings!</p>
    </div>
  );
}

function App() {
  return (
    <SettingsProvider>
      <div>
        <h1>App Settings Demo</h1>
        <SettingsPanel />
        <DisplayContent />
      </div>
    </SettingsProvider>
  );
}

Custom Hook Pattern (Best Practice)

// Create this in a separate file like 'AuthContext.js'
import React, { createContext, useState, useContext } from 'react';

const AuthContext = createContext();

export function 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(false);

  const login = async (email, password) => {
    setLoading(true);
    // Simulate API call
    setTimeout(() => {
      setUser({ email, name: 'John Doe', token: 'abc123' });
      setLoading(false);
    }, 1000);
  };

  const logout = () => {
    setUser(null);
  };

  const value = {
    user,
    login,
    logout,
    loading,
    isAuthenticated: !!user
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// In your components:
function LoginButton() {
  const { login, loading } = useAuth();

  const handleLogin = () => {
    login('user@example.com', 'password123');
  };

  return (
    <button onClick={handleLogin} disabled={loading}>
      {loading ? 'Logging in...' : 'Login'}
    </button>
  );
}

function UserProfile() {
  const { user, logout } = useAuth();

  if (!user) return <p>Please login</p>;

  return (
    <div>
      <p>Welcome, {user.name}!</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

When to Use useContext?

✅ USE useContext when:

  • Multiple components need the same data

  • You're passing props through 3+ levels (prop drilling)

  • You have global data (theme, user auth, language)

  • Many components need to update the same state

❌ DON'T use useContext for:

  • Data used by only parent-child components (just use props)

  • Data that changes very frequently (performance issues)

  • Simple state that's only used in one component

Common Mistakes

Mistake 1: Forgetting the Provider

// ❌ WRONG: Component outside Provider gets undefined
function Component() {
  const value = useContext(MyContext); // value = undefined
  return <p>{value}</p>;
}

// ✅ CORRECT: Wrap in Provider
function App() {
  return (
    <MyContext.Provider value="Hello">
      <Component />
    </MyContext.Provider>
  );
}

Mistake 2: Unnecessary Rerenders

// ❌ WRONG: New object every render causes rerenders
function App() {
  const [user, setUser] = useState({ name: 'John' });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Child />
    </UserContext.Provider>
  );
  // New {user, setUser} object every time = Child rerenders every time
}

// ✅ BETTER: Memoize the value
function App() {
  const [user, setUser] = useState({ name: 'John' });

  const contextValue = useMemo(() => ({ user, setUser }), [user]);

  return (
    <UserContext.Provider value={contextValue}>
      <Child />
    </UserContext.Provider>
  );
}

Practice Exercise: Build a Notification System

Create a notification system where:

  1. Any component can show notifications

  2. A NotificationCenter component displays all notifications

  3. Notifications auto-remove after 3 seconds

// Try this structure:
const NotificationContext = createContext();

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const addNotification = (message) => {
    // Your code here
  };

  const removeNotification = (id) => {
    // Your code here
  };

  return (
    <NotificationContext.Provider value={{ addNotification }}>
      {children}
      <NotificationCenter notifications={notifications} />
    </NotificationContext.Provider>
  );
}

// Then use in components:
function AddToCartButton() {
  const { addNotification } = useContext(NotificationContext);

  const handleClick = () => {
    // Add item to cart
    addNotification('Item added to cart!');
  };

  return <button onClick={handleClick}>Add to Cart</button>;
}

Summary Cheat Sheet

// 1. CREATE context (usually in separate file)
const MyContext = createContext(defaultValue);

// 2. PROVIDE value (wrap your app/components)
<MyContext.Provider value={someValue}>
  <ChildComponents />
</MyContext.Provider>

// 3. CONSUME value (in any child component)
const value = useContext(MyContext);

// 4. Best Practice: Create custom hook
function useMyContext() {
  return useContext(MyContext);
}

Remember: useContext is like a shortcut through the component tree. It lets deeply nested components access data without every middle component needing to pass it along!