Context API

Course 2 · Ch 2
Context API and useContext
Sharing a value with any component nested below, with no props passed through the layers in between

The shopping cart project briefly used Context to solve a real problem: a header badge, a product grid, and a cart page all needing the same data despite sitting nowhere near each other in the component tree. This chapter covers that mechanism properly — Context as React's built-in answer to the prop-drilling problem first raised back in Fundamentals Chapter 10.

The Three Pieces

import { createContext, useContext, useState } from "react"; // 1. Create the context const ThemeContext = createContext(null); // 2. Provide a value, somewhere up the tree function App() { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Toolbar /> </ThemeContext.Provider> ); } // 3. Read it, anywhere below — no matter how deep function ThemeButton() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Current theme: {theme} </button> ); }

createContext(null) creates the context object itself, separately from any actual data — the null is just a fallback default. <ThemeContext.Provider value={...}> makes that value available to everything nested inside it; ThemeButton can sit any number of components below App — even with several unrelated components in between — and still reach the same theme/setTheme directly via useContext(ThemeContext), without a single one of those in-between components ever mentioning theme at all.

Providing State and Functions Together

The shopping cart project's pattern — bundling state and the functions that update it into one object passed to value — is the standard shape for a real context, not just a single raw value. It's common enough to extract the whole thing into its own dedicated "provider" component, exactly as CartProvider did:

function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); function toggleTheme() { setTheme((t) => (t === "light" ? "dark" : "light")); } return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }

ThemeProvider wraps {children} (the composition pattern from Fundamentals Chapter 9) rather than rendering anything visible itself — its entire job is owning the state and exposing it, leaving every consumer free to use it however it wants.

A Custom Hook for Cleaner Consumption

function useTheme() { return useContext(ThemeContext); } // usage — consumers never need to import ThemeContext directly const { theme, toggleTheme } = useTheme();

Wrapping useContext(ThemeContext) in a small custom hook is an extremely common pattern in real codebases — it means every consumer imports one clearly-named function (useTheme) instead of needing to know the underlying context object exists at all. Custom hooks get their own full treatment next chapter; this is a first glimpse of how naturally they pair with Context specifically.

Context isn't free, and it isn't for everything
Every component consuming a context re-renders whenever that context's value changes — for state that changes rarely (theme, the logged-in user, locale), that's entirely fine. For state that changes very frequently (every keystroke, every animation frame), Context can cause far more re-rendering than passing props directly to just the few components that need it. When in doubt: reach for lifting state up first, and only bring in Context once prop drilling genuinely becomes painful, exactly as Fundamentals Chapter 10 first suggested.
SituationReach for...
A few nearby components share stateLifting state up (props)
Many distant components need the same rarely-changing valueContext
State changes very frequently and widely consumedA dedicated state library (Advanced Ch 1)

Coding Challenges

Challenge 1

Build a ThemeContext with a ThemeProvider holding theme ("light"/"dark") and a toggleTheme function. Consume it in at least two unrelated components (e.g. a Header and a Footer) so both reflect the same theme and both have a way to toggle it.

📄 View solution
Challenge 2

Build an AuthContext storing a user (null when logged out, an object like { name } when logged in) plus login and logout functions. Consume it in a Header (showing "Welcome, [name]" or a login button) and a separate Dashboard component (showing content only when a user is logged in).

📄 View solution
Challenge 3

Take your ThemeContext from Challenge 1 and write a useTheme custom hook wrapping useContext(ThemeContext). Refactor every consumer to call useTheme() instead of useContext(ThemeContext) directly.

📄 View solution

Chapter 2 Quick Reference

  • createContext(default) — creates the context object itself
  • <Context.Provider value={...}> — makes a value available to every nested consumer
  • useContext(Context) — reads that value, from any depth, with no props passed through
  • Bundle state + its updater functions into one value object — the standard shape for a real context
  • A small custom hook (useTheme) wrapping useContext is a common, cleaner consumption pattern
  • Context re-renders every consumer on change — best for rarely-changing, widely-needed values, not high-frequency state
  • Next chapter: useReducer — managing more complex state transitions with a single dispatch function