React · State at Scale

Context API

useContext lets components read shared data. The Context API pattern turns that into a tidy module: a provider that owns the state and a custom hook that exposes it.

Intermediate 6 min Lesson 26 of 42Examples target React 19 with Vite.
Before you start:useContextuseReducer
0/42 in React
On this page · 6 min · Intermediate

Learning objectives#

  • Write a provider component that owns state
  • Expose context through a custom hook with a safety check
  • Combine context with useReducer
  • Split contexts to avoid unnecessary re-renders

Explanation#

Pattern: one file per context. It creates the context, exports a Provider component that holds state, and exports a useX() hook for consumers.

The hook can throw a clear error when used outside the provider — far easier to debug than a silent default value.

For complex updates, the provider uses useReducer and exposes dispatch or named actions (addItem, removeItem).

Every consumer re-renders when the context value changes. Split state and actions into two contexts, or split unrelated data (auth vs cart), so components only re-render for what they use.

Context is not a replacement for all state. Keep local state local; use context for things many distant components need.

Syntax#

JSX Illustrative
const CartContext = createContext(null);


export function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  return <CartContext value={{ items, setItems }}>{children}</CartContext>;
}


export function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error('useCart must be used inside <CartProvider>');
  return ctx;
}

Code example: A cart context with reducer and custom hook#

JSXCartContext.jsx Runnable
import { createContext, useContext, useMemo, useReducer } from 'react';


const CartContext = createContext(null);


function cartReducer(items, action) {
  switch (action.type) {
    case 'add': {
      const found = items.find(i => i.id === action.product.id);
      return found
        ? items.map(i => (i.id === found.id ? { ...i, qty: i.qty + 1 } : i))
        : [...items, { ...action.product, qty: 1 }];
    }
    case 'remove':
      return items.filter(i => i.id !== action.id);
    default:
      throw new Error('Unknown action ' + action.type);
  }
}


export function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    count: items.reduce((n, i) => n + i.qty, 0),
    total: items.reduce((s, i) => s + i.qty * i.price, 0),
    add: product => dispatch({ type: 'add', product }),
    remove: id => dispatch({ type: 'remove', id }),
  }), [items]);
  return <CartContext value={value}>{children}</CartContext>;
}


export function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error('useCart must be used inside <CartProvider>');
  return ctx;
}


// ── Consumers anywhere in the tree ──
function CartBadge() {
  const { count, total } = useCart();
  return <p>🛒 {count} items · ₹{total}</p>;
}


function ProductButton({ product }) {
  const { add } = useCart();
  return <button onClick={() => add(product)}>Add {product.name}</button>;
}


export default function App() {
  return (
    <CartProvider>
      <CartBadge />
      <ProductButton product={{ id: 1, name: 'Pen', price: 20 }} />
      <ProductButton product={{ id: 2, name: 'Bag', price: 650 }} />
    </CartProvider>
  );
}
Expected output
The badge starts at “🛒 0 items · ₹0”. Clicking “Add Pen” twice and “Add Bag” once makes it “🛒 3 items · ₹690”. The buttons and the badge never pass props to each other — they share the cart through the provider.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. CartProvider owns the state with useReducer and builds the value object.
  2. count and total are derived inside the provider so every consumer gets the same numbers.
  3. useMemo keeps the value object stable unless items change.
  4. useCart gives consumers the cart and throws a helpful error outside the provider.
  5. CartBadge and ProductButton just call useCart().

Real-world use case#

An e-commerce front end has AuthProvider, CartProvider and ThemeProvider wrapping the router in main.jsx. Product pages, the header and checkout all use useAuth() and useCart().

Common mistakes#

Using the hook outside the provider

Without the check, consumers silently get null and crash later. Throw a clear error in the hook.

One giant AppContext

Putting user, cart, theme and notifications in one value re-renders everything on any change. Split by concern.

Exporting the raw context

Consumers depend on its shape. Export a hook and keep the context private to the file.

Best practices#

  • One file per context: provider + hook
  • Throw when used outside the provider
  • Derive values inside the provider
  • Split contexts by concern

Remember this#

Provider owns state; hook exposes it.

useX() outside <XProvider> → clear error.

Many unrelated values → many small contexts.

Practice exercise#

Your task: Write an AuthProvider with user (null by default), login(name) and logout(), plus a useAuth hook. Show "Hi, Asha" with a Logout button, or a Login button.

Need a hint?
useState(null) for user; login sets { name }, logout sets null.
Show solution
JSX Runnable
import { createContext, useContext, useState } from 'react';


const AuthContext = createContext(null);


function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = { user, login: name => setUser({ name }), logout: () => setUser(null) };
  return <AuthContext value={value}>{children}</AuthContext>;
}


function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>');
  return ctx;
}


function UserMenu() {
  const { user, login, logout } = useAuth();
  return user
    ? <p>Hi, {user.name} <button onClick={logout}>Logout</button></p>
    : <button onClick={() => login('Asha')}>Login</button>;
}


export default function App() {
  return <AuthProvider><UserMenu /></AuthProvider>;
}

Mini quiz#

Context API quizMultiple choice Question 1 of 3

Why export a useCart() hook instead of the CartContext object?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.