cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Scaling up with a reducer and context

Manage a whole screen's state with a reducer and context: a provider component, useCart-style custom hooks, and why state and dispatch get separate contexts.

By the end, you can
  • 01
    Put a reducer's state and dispatch in contextProvide the state and the dispatch function from one component, so any component below can read the state or send an action without props.
  • 02
    Wrap the wiring in a provider and hooksMove the reducer and the contexts into one file with a provider component and custom hooks like useCart and useCartDispatch.
  • 03
    Explain why state and dispatch are splitSay why components that only send actions read a separate context, and which components render again after an action.

A cart that every part of the shop needs

A shop screen has a cart badge in the header, Add buttons on every product, and later a cart panel with Remove buttons. They all need the same cart. A reducer keeps the cart's update rules in one function, and useReducer gives you the cart and dispatch, the function that sends the reducer an action. Here they live in App and travel down as props. Click both Add buttons in the result:

The cart and dispatch passed down through every layerPop out in the code editor (opens in a new tab)JSX
import { useReducer } from "react"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },]; function cartReducer(cart, action) {  switch (action.type) {    case "added":      return [...cart, action.tea];    default:      throw Error("Unknown action: " + action.type);  }} function CartBadge({ cart }) {  return <p>Cart: {cart.length}</p>;} function Header({ cart }) {  return (    <header>      <h2>Tea Corner</h2>      <CartBadge cart={cart} />    </header>  );} function ProductCard({ tea, dispatch }) {  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} function ProductList({ dispatch }) {  return (    <ul>      {teas.map((tea) => (        <ProductCard key={tea.id} tea={tea} dispatch={dispatch} />      ))}    </ul>  );} export default function App() {  const [cart, dispatch] = useReducer(cartReducer, []);  return (    <>      <Header cart={cart} />      <ProductList dispatch={dispatch} />    </>  );}
ResultLoading…

Lines 8 to 15 are the reducer: for an added action, it returns a new array with the tea at the end. Line 50 creates the cart, starting empty, and dispatch. Line 53 passes the cart to Header, which passes it on to CartBadge on line 25. Line 54 passes dispatch to ProductList, which passes it on to each ProductCard on line 43. The click on line 34 sends { type: "added", tea }, and the badge on line 18 counts the cart: after two clicks, Cart: 2.

It works, but look at Header and ProductList. Neither uses the cart or dispatch; they take them only to hand them on. That's called prop drilling. With a real screen, ten layers deep, every layer in between would carry props it never uses, and moving a component means rewiring them all.

You've met both tools already. A reducer gathers the state's update rules, and context lets a component provide a value to every component below it, however deep. The two lessons before this one, Extracting state logic into a reducer and Passing data deeply with context, teach each on its own. Put together, the component with the reducer provides the cart and dispatch in context, and any component below reads what it needs:

Two trees side by side. Left, props: App holds cart and dispatch, passes cart to Header, which passes it to CartBadge, and passes dispatch to ProductList, which passes it to ProductCard. Header and ProductList only pass them on. Right, context: a CartProvider window holds the same tree, Header above CartBadge and ProductList above ProductCard, with no props; CartBadge reads the cart and ProductCard reads dispatch.props through every layerAppcartdispatchHeaderProductListcartdispatchCartBadgeProductCardthe middle layers only pass them oncontextCartProviderHeaderProductListCartBadgeProductCardreads cartreads dispatchanything inside can read them
Left: with props, Header and ProductList take the cart and dispatch only to hand them on. Right: with context, the provider makes them available to everything inside it, and CartBadge and ProductCard read them directly.
Real-life analogyA restaurant's order board

In a restaurant, waiters don't pass order slips hand to hand to reach the kitchen. Any waiter drops a slip in the ticket slot, the kitchen follows its own rules to update the orders, and the board on the wall shows the new list to everyone who looks.

In real life: The kitchen, which keeps the orders and decides what happens to them
In React: The provider component, with useReducer and cartReducer
In real life: The order board on the wall, which anyone can look at
In React: CartContext, which any component below can read
In real life: The ticket slot, where any waiter drops an order slip
In React: CartDispatchContext, which gives any component dispatch
In real life: An order slip: "table 4, one green tea"
In React: An action: { type: "added", tea }
The main idea

Combine a reducer with context to manage the state of a whole screen. One component holds the state with useReducer and provides it, and the dispatch function, through two contexts. Any component below reads the state or sends actions, with no props in between.

Putting the cart and dispatch in context

You can move a reducer's state and dispatch into context in three steps.

Step 1: create two contexts. One carries the cart, the other carries dispatch. They live in their own file, so any component file can import them. This example has two files, in tabs: App.jsx and CartContext.js. In CartContext.js, lines 3 and 4 create the two contexts with createContext(null). The null is the default value, used only by a component that has no provider above it.

The cart and dispatch provided by App and read where neededPop out in the code editor (opens in a new tab)JSX
import { useContext, useReducer } from "react";import { CartContext, CartDispatchContext } from "./CartContext.js"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },]; function cartReducer(cart, action) {  switch (action.type) {    case "added":      return [...cart, action.tea];    default:      throw Error("Unknown action: " + action.type);  }} function CartBadge() {  const cart = useContext(CartContext);  return <p>Cart: {cart.length}</p>;} function Header() {  return (    <header>      <h2>Tea Corner</h2>      <CartBadge />    </header>  );} function ProductCard({ tea }) {  const dispatch = useContext(CartDispatchContext);  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} function ProductList() {  return (    <ul>      {teas.map((tea) => (        <ProductCard key={tea.id} tea={tea} />      ))}    </ul>  );} export default function App() {  const [cart, dispatch] = useReducer(cartReducer, []);  return (    <CartContext value={cart}>      <CartDispatchContext value={dispatch}>        <Header />        <ProductList />      </CartDispatchContext>    </CartContext>  );}
ResultLoading…

Step 2: provide them. In App.jsx, line 53 still calls useReducer. Lines 55 and 56 wrap the screen in both contexts: <CartContext value={cart}> gives every component inside the current cart, and <CartDispatchContext value={dispatch}> gives them dispatch.

Step 3: read them where they're needed. Line 19 reads the cart in CartBadge with useContext(CartContext), and line 33 reads dispatch in ProductCard. useContext finds the closest provider above the component and returns its value. Now Header (lines 23 to 30) and ProductList (lines 42 to 50) take no props at all. Click both Add buttons: Cart: 2, the same as before.

Context as a provider, new in React 19

Since React 19, you render the context itself as its provider: <CartContext value={cart}>. Code written for earlier versions writes <CartContext.Provider value={cart}>. That still works in React 19, but React plans to deprecate it, so write the shorter form in new code.

The state still lives in App, in its useReducer. Context changes only how it travels. How to read the table: each row asks one question, answered for props and for context.

Passing the cart as props, or providing it in context
QuestionProps through every layerContext
Who passes the cart along?Every component between App and the ones that use it.Nobody. The provider puts it in context once.
Which components mention the cart?The ones that use it, and every layer in between.Only the ones that use it.
What happens when you add a layer?The new layer must take the props and pass them on.Nothing changes. Context reaches through any number of layers.
How do you find where the cart comes from?Follow the props up, one parent at a time.Find the closest provider above the component.

How an action travels through the screen

With the cart in context, data still flows one way. The cart goes down from the provider to the components that read it. An action goes up: a component calls dispatch, React runs the reducer with the current cart and the action, and the reducer returns the next cart. Then the provider renders with it, and every component that reads the cart renders again.

CartProvider at the top runs useReducer with cartReducer. On the left, an arrow goes up from ProductCard to CartProvider, labelled 1, dispatch an added action. Under CartProvider, a label says 2, cartReducer returns the new cart. Arrows go down from CartProvider to CartBadge and CartPanel, labelled 3, cart.CartProvideruseReducer(cartReducer)2. returns the new cartProductCard1. dispatch({ type: "added" })CartBadgeCartPanel3. cartcart
1. ProductCard sends an action with dispatch. 2. CartProvider's reducer works out the new cart. 3. The new cart goes down through CartContext, and the components that read it, CartBadge and CartPanel, render again.

Try it on a whole shop screen. It has three products and a cart panel whose buttons remove one tea or empty the cart. Three different components send actions, and they all go to the same reducer:

Playground: follow an action
The shopclick it

Tea Corner

Cart: 0

  • Sencha: $4
  • Assam: $3
  • Mint tea: $5

Your cart

Nothing here yet.

    Total: $0

    Actions sent to cartReducer0 shown

    No actions yet. The cart is [].

    Try it yourself

    Use the shop: Add, Remove, and Empty cart.

    Press Add Sencha, Add Assam, then Remove Sencha in the cart. Each action, from whichever component, goes to the same reducer.

    The shop screen from this lesson runs here, with React on this page: the same contexts and reducer. The log shows the last six actions: who sent each one, the action object, and the cart the reducer returned. Reset empties the cart and the log.

    Press Add Sencha and Add Assam: ProductCard sends two added actions, and the cart becomes [Sencha, Assam]. Press Remove Sencha in the panel: CartPanel sends { type: "removed", index: 0 }, and the cart is [Assam]. Neither component knows the other exists. They both know only dispatch, and the reducer decides what each action does to the cart.

    Moving the wiring into a CartProvider

    App still does a lot: it calls useReducer and nests two providers around the screen. You can move all of that into one component, CartProvider, in the contexts' file. Open the CartContext.jsx tab:

    A CartProvider component holds the reducer and both contextsPop out in the code editor (opens in a new tab)JSX
    import { useContext } from "react";import { CartContext, CartDispatchContext, CartProvider } from "./CartContext.jsx"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },]; function CartBadge() {  const cart = useContext(CartContext);  return <p>Cart: {cart.length}</p>;} function ProductCard({ tea }) {  const dispatch = useContext(CartDispatchContext);  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} export default function App() {  return (    <CartProvider>      <header>        <h2>Tea Corner</h2>        <CartBadge />      </header>      <ul>        {teas.map((tea) => (          <ProductCard key={tea.id} tea={tea} />        ))}      </ul>    </CartProvider>  );}
    ResultLoading…

    In CartContext.jsx, lines 6 to 15 are CartProvider. It does three jobs:

    • Line 7 manages the cart with the reducer, which moved into this file too (lines 17 to 28).
    • Lines 9 and 10 provide both contexts.
    • Line 11 renders children: whatever App puts between <CartProvider> and </CartProvider>. (Props covers the children prop.)

    In App.jsx, line 2 imports CartProvider with the contexts, and App (lines 24 to 38) only wraps the screen in it, from line 26. App has no state and no reducer now. Press Edit & run, open CartContext.jsx, and change the [] on line 7 to [{ id: "mint", name: "Mint tea", price: 5 }]: the badge starts at Cart: 1. Only the provider knows how the cart starts.

    Think of CartProvider as the part of the screen that knows how to deal with the cart. The state now lives in CartProvider. Because App renders it near the top, it's still above every component that needs the cart: the same place Sharing state between components taught you to put shared state, the closest common parent.

    Custom hooks: useCart and useCartDispatch

    Components still import two context objects and call useContext with the right one. You can hide that behind two small functions in the same file, and stop exporting the contexts:

    CartContext.jsx exports a provider and two hooksPop out in the code editor (opens in a new tab)JSX
    import { CartProvider, useCart, useCartDispatch } from "./CartContext.jsx"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },]; function CartBadge() {  const cart = useCart();  return <p>Cart: {cart.length}</p>;} function ProductCard({ tea }) {  const dispatch = useCartDispatch();  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} export default function App() {  return (    <CartProvider>      <header>        <h2>Tea Corner</h2>        <CartBadge />      </header>      <ul>        {teas.map((tea) => (          <ProductCard key={tea.id} tea={tea} />        ))}      </ul>    </CartProvider>  );}
    ResultLoading…

    In CartContext.jsx, lines 3 and 4 create the contexts without export: no other file can reach them now. useCart (lines 17 to 23) reads the cart context and returns the cart. useCartDispatch (lines 25 to 31) does the same for dispatch. In App.jsx, line 9 is now const cart = useCart(); and line 14 is const dispatch = useCartDispatch();. The components say what they need, not where it comes from.

    Functions like these are custom hooks: functions you write whose names start with use. The name tells you, and React's linter, that the function calls other hooks, like useContext, so it follows the same rules as they do: call it at the top level of a component, not inside a condition or a loop. A later lesson, Reusing logic with custom hooks, writes many more.

    A hook can also check how it's used. Lines 19 to 21 throw an error when the cart is null. That happens only when no CartProvider sits above the component, because null is the default value from createContext(null). Try it: press Edit & run, and in App.jsx change <CartProvider> on line 25 to <> and </CartProvider> on line 35 to </>. The result shows the error, useCart must be used inside a CartProvider, in place of the page. Without the check, the badge would fail with a less helpful TypeError about reading length of null. Put the provider back, and the shop returns.

    A window titled CartContext.jsx. Inside, on the left, three cards kept in the file: cartReducer, CartContext, CartDispatchContext. On the right, three exported cards: CartProvider, useCart, and useCartDispatch. Arrows lead out of the window to the components that use them: App uses CartProvider, CartBadge uses useCart, ProductCard uses useCartDispatch.CartContext.jsxkept insidecartReducerCartContextCartDispatchContextexportedCartProvideruseCart()useCartDispatch()AppCartBadgeProductCard
    CartContext.jsx keeps the reducer and both contexts to itself. It exports three things: CartProvider for App to wrap the screen in, and two hooks for the components that read the cart or send actions.

    Why the cart and dispatch get separate contexts

    Why two contexts and not one, holding { cart, dispatch }? Both work. The difference is which components render again after an action, and it comes from two facts:

    • dispatch never changes. React gives you the same function on every render of CartProvider, for as long as it's on the screen.
    • When a provider's value changes, React renders again every component that reads that context, as Passing data deeply with context showed. It compares the old and new value with Object.is, so a new object counts as a change, even if what's inside is the same.

    With one context, the value is a new { cart, dispatch } object after every change to the cart, so every reader renders again, even a ProductCard that only dispatches. With two, the dispatch context's value stays the same function, and the components that only dispatch are left alone. Try both. The shop below counts each component's renders:

    Playground: one context or two?
    CartProvider and its hooksJSX
    const CartContext = createContext(null);
    
    export function CartProvider({ children }) {
      const [cart, dispatch] = useReducer(cartReducer, []);
      return (
        <CartContext value={{ cart, dispatch }}>
          {children}
        </CartContext>
      );
    }
    
    export function useCart() {
      return useContext(CartContext).cart;
    }
    
    export function useCartDispatch() {
      return useContext(CartContext).dispatch;
    }
    The shopclick Add

    Tea Corner

    Cart: 0

    • Sencha: $4
    • Assam: $3

    Total: $0

    Rendersone context
    1. CartProviderthe reducer · renders: 0
    2. Headernothing · renders: 0
    3. CartBadgeuseCart() · renders: 0
    4. ProductListnothing · renders: 0
    5. ProductCard (Sencha)useCartDispatch() · renders: 0
    6. ProductCard (Assam)useCartDispatch() · renders: 0
    7. CartTotaluseCart() · renders: 0
    Try it yourself

    Press Add Sencha in the shop, a few times. Then switch to the other way and do the same.

    The shop runs here, with React on this page, and its CartProvider is written the way the code shows. Each component reports a render when React commits it, so StrictMode's extra development call isn't counted. The counts start at 1, the first render. Choosing a way, or Reset, starts a fresh shop.

    With One context, one Add click renders five components: CartProvider, CartBadge, CartTotal, and both ProductCards. With Two contexts, it's three: the cards are skipped. In both, Header and ProductList stay at 1. They read no context, and they came to CartProvider as children, made by App, which didn't render. So nothing gave them new props.

    Two trees after an Add click. Left, one context holding cart and dispatch: CartProvider, CartBadge, which calls useCart, and ProductCard, which calls useCartDispatch, all render. Right, two contexts: CartProvider and CartBadge render; ProductCard is crossed out as skipped.one contextCartProviderCartBadgeProductCarduseCart()useCartDispatch()rendersrenderstwo contextsCartProviderCartBadgeProductCarduseCart()useCartDispatch()rendersskipped
    After one Add click. Left: with one context, its value is a new { cart, dispatch } object, so ProductCard renders again too. Right: with two, ProductCard reads only dispatch, which didn't change, so React skips it.

    How to read the table: each row asks one question, answered for one context and for two.

    One context or two
    QuestionOne context: { cart, dispatch }Two contexts
    What is the context's value?A new object, { cart, dispatch }, made on every render of the provider.cart in one context, and dispatch alone in the other.
    When does the value change?Every time the cart changes, for every component that reads it.cart changes when the cart changes. dispatch never changes.
    Who renders again after Add?Every component that reads the context, even ones that only dispatch.Only the components that read the cart.
    What do components write?useCart() and useCartDispatch().The same: the hooks hide which context they read.

    In a small app, the extra renders cost nothing you'd notice, and one context is fine. On a big screen with many components that only send actions, splitting saves real work, and it costs nothing: the hooks hide it. The When React re-renders lesson, in the Performance stage, looks at renders in detail.

    A whole shop screen, one component per file

    Here is the shop screen as you'd write it in a project: one file per component, and CartContext.jsx holding all the cart's wiring. Click the tabs to read each file, then add a few teas, remove one, and empty the cart:

    A shop screen wired through CartContext.jsxPop out in the code editor (opens in a new tab)JSX
    import { CartProvider } from "./CartContext.jsx";import Header from "./Header.jsx";import ProductList from "./ProductList.jsx";import CartPanel from "./CartPanel.jsx"; export default function App() {  return (    <CartProvider>      <Header />      <ProductList />      <CartPanel />    </CartProvider>  );}
    ResultLoading…

    App.jsx is only a layout: lines 8 to 12 put Header, ProductList, and CartPanel inside CartProvider. In Header.jsx, line 4 reads the cart for the badge. In ProductList.jsx, line 10 gets dispatch for the Add buttons. CartPanel.jsx needs both: lines 4 and 5. Its Remove buttons send removed with the tea's position on line 15, and Empty cart sends cleared on line 22. Each line in the panel has no state of its own, so its position works as a key here; Rendering lists explains when you need a real id.

    None of these files passes the cart around. To add a wish list next to the cart later, you'd write a second pair the same way: a SavedProvider with useSaved and useSavedDispatch. Apps often have several such pairs, each near the top of the part of the screen that needs it. Exercise 5 builds one.

    Which hook does it need?
    • CartBadge shows how many teas are in the cart
    • ProductCard has an Add button
    • ClearButton empties the cart
    • CartTotal shows the sum of the prices
    • CartPanel lists the teas, each with a Remove button
    • CheckoutButton is off while the cart is empty, and empties it when pressed
    • Header only lays out the shop's name and the badge
    • Footer shows the shop's opening hours
    Try it yourself
    0 of 8 correct

    Sort each component: does it read the cart, send actions, both, or neither?

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    Common mix-ups

    • "Context stores the state." No. The state lives in the component that calls useReducer, here CartProvider. Context only carries its value down to the components below.
    • "A reducer and context replace props everywhere." No. Props are still the clearest way to give a child its own data, like the tea each ProductCard gets. Use context for state that many distant components share.
    • "Every component that calls useCartDispatch renders again after each action." No. With its own context, dispatch never changes, so those components are skipped.
    • "A component outside the provider gets the cart anyway." No. It gets the default value from createContext. With null and a check in the hook, you get a clear error instead of a wrong page.
    • "Custom hooks are a special React feature." No. They're your own functions. Starting the name with use marks them as calling hooks, so the same rules apply to them.
    • "Context is a global variable." No. A component reads the value from the closest provider above it. Two CartProviders on one page would hold two separate carts.

    Practice exercises

    For each one, find where the state lives and which components read it or send actions. Press Edit & run to change the code; the result updates as you type.

    Exercise 1 · Warm-upPredict the total

    You click Add Sencha twice. What does the total say? Answer first, then press Edit & run and click to check.

    import { createContext, useContext, useReducer } from "react";
    
    const CartContext = createContext(null);
    const CartDispatchContext = createContext(null);
    
    function cartReducer(cart, action) {
      switch (action.type) {
        case "added":
          return [...cart, action.tea];
        default:
          throw Error("Unknown action: " + action.type);
      }
    }
    
    function CartTotal() {
      const cart = useContext(CartContext);
      const total = cart.reduce((sum, tea) => sum + tea.price, 0);
      return <p>Total: ${total}</p>;
    }
    
    function AddSencha() {
      const dispatch = useContext(CartDispatchContext);
      const sencha = { id: "sencha", name: "Sencha", price: 4 };
      return <button onClick={() => dispatch({ type: "added", tea: sencha })}>Add Sencha</button>;
    }
    
    export default function App() {
      const [cart, dispatch] = useReducer(cartReducer, [
        { id: "mint", name: "Mint tea", price: 5 },
      ]);
      return (
        <CartContext value={cart}>
          <CartDispatchContext value={dispatch}>
            <CartTotal />
            <AddSencha />
          </CartDispatchContext>
        </CartContext>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 2 · Warm-upA button that only sends an action

      Add a ClearButton component in the header that empties the cart. Then click Add Sencha, Add Assam, and Clear. What does the badge say?

      import { CartProvider, useCart, useCartDispatch } from "./CartContext.jsx"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },]; function CartBadge() {  const cart = useCart();  return <p>Cart: {cart.length}</p>;} function ProductCard({ tea }) {  const dispatch = useCartDispatch();  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} // Write ClearButton here. export default function App() {  return (    <CartProvider>      <header>        <h2>Tea Corner</h2>        <CartBadge />      </header>      <ul>        {teas.map((tea) => (          <ProductCard key={tea.id} tea={tea} />        ))}      </ul>    </CartProvider>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 3 · PracticeA badge outside the provider

        This shop shows an error instead of the page. Read App and useCart: what does the error say? Press Edit & run to check, then fix the shop.

        import { CartProvider, useCart, useCartDispatch } from "./CartContext.jsx";
        
        const teas = [
          { id: "sencha", name: "Sencha", price: 4 },
          { id: "assam", name: "Assam", price: 3 },
        ];
        
        function CartBadge() {
          const cart = useCart();
          return <p>Cart: {cart.length}</p>;
        }
        
        function ProductCard({ tea }) {
          const dispatch = useCartDispatch();
          return (
            <li>
              {tea.name}: ${tea.price}{" "}
              <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>
            </li>
          );
        }
        
        export default function App() {
          return (
            <>
              <header>
                <h2>Tea Corner</h2>
                <CartBadge />
              </header>
              <CartProvider>
                <ul>
                  {teas.map((tea) => (
                    <ProductCard key={tea.id} tea={tea} />
                  ))}
                </ul>
              </CartProvider>
            </>
          );
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 4 · PracticeTeach the reducer to remove

          The Remove buttons break the page. Fix the reducer in the CartContext.jsx tab. After your fix, click Add Sencha, Add Assam, then Remove Sencha. Which tea is left?

          import { CartProvider } from "./CartContext.jsx";import Header from "./Header.jsx";import ProductList from "./ProductList.jsx";import CartPanel from "./CartPanel.jsx"; export default function App() {  return (    <CartProvider>      <Header />      <ProductList />      <CartPanel />    </CartProvider>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 5 · ChallengeMove a saved list into context

            The saved list travels through Header and ProductList as props. Move it into a SavedProvider with useSaved and useSavedDispatch hooks, in a new file. (In Edit & run, you can also keep everything in App.jsx.) Then click Save Sencha, Save Mint tea, and Sencha's button again. What does the header say?

            import { useReducer } from "react"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },  { id: "mint", name: "Mint tea", price: 5 },]; function savedReducer(saved, action) {  switch (action.type) {    case "toggled":      return saved.includes(action.id)        ? saved.filter((id) => id !== action.id)        : [...saved, action.id];    default:      throw Error("Unknown action: " + action.type);  }} function SavedCount({ saved }) {  return <p>Saved: {saved.length}</p>;} function Header({ saved }) {  return (    <header>      <h2>Tea Corner</h2>      <SavedCount saved={saved} />    </header>  );} function SaveButton({ tea, saved, dispatch }) {  const isSaved = saved.includes(tea.id);  return (    <button onClick={() => dispatch({ type: "toggled", id: tea.id })}>      {isSaved ? "Saved" : "Save"} {tea.name}    </button>  );} function ProductList({ saved, dispatch }) {  return (    <ul>      {teas.map((tea) => (        <li key={tea.id}>          <SaveButton tea={tea} saved={saved} dispatch={dispatch} />        </li>      ))}    </ul>  );} export default function App() {  const [saved, dispatch] = useReducer(savedReducer, []);  return (    <>      <Header saved={saved} />      <ProductList saved={saved} dispatch={dispatch} />    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 6 · ChallengeA useCartTotal hook

              Two components work out the total themselves. Write a useCartTotal hook in CartContext.jsx and use it in both. Then click Add Sencha and Add Mint tea. What does the checkout button say?

              import { CartProvider, useCart, useCartDispatch } from "./CartContext.jsx"; const teas = [  { id: "sencha", name: "Sencha", price: 4 },  { id: "assam", name: "Assam", price: 3 },  { id: "mint", name: "Mint tea", price: 5 },]; function ProductCard({ tea }) {  const dispatch = useCartDispatch();  return (    <li>      {tea.name}: ${tea.price}{" "}      <button onClick={() => dispatch({ type: "added", tea })}>Add {tea.name}</button>    </li>  );} function CartTotal() {  const cart = useCart();  const total = cart.reduce((sum, tea) => sum + tea.price, 0);  return <p>Total: ${total}</p>;} function CheckoutButton() {  const cart = useCart();  const total = cart.reduce((sum, tea) => sum + tea.price, 0);  return <button disabled={total === 0}>Pay ${total}</button>;} export default function App() {  return (    <CartProvider>      <ul>        {teas.map((tea) => (          <ProductCard key={tea.id} tea={tea} />        ))}      </ul>      <CartTotal />      <CheckoutButton />    </CartProvider>  );}
              ResultLoading…

              Answer, then press Check. Spacing and letter case don’t matter.

                Check your understanding

                For each question, find the provider, then ask which components sit inside it and what each one reads.

                The reducer and context quiz · 7 questionsScore: first tries count
                1. Question 1 of 7Why does the lesson put cart and dispatch in two separate contexts?

                  Choose an answer to see the explanation.

                2. Question 2 of 7CartContext.jsx is the lesson's final file, with useCart. What does this page show?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                  import { CartProvider, useCart } from "./CartContext.jsx";
                  
                  function CartBadge() {
                    const cart = useCart();
                    return <p>Cart: {cart.length}</p>;
                  }
                  
                  export default function App() {
                    return (
                      <>
                        <CartBadge />
                        <CartProvider>
                          <p>Teas go here.</p>
                        </CartProvider>
                      </>
                    );
                  }

                  Choose an answer to see the explanation.

                3. Question 3 of 7This context's default value is []. What does the badge show?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                  import { createContext, useContext } from "react";
                  
                  const CartContext = createContext([]);
                  
                  function CartBadge() {
                    const cart = useContext(CartContext);
                    return <p>Cart: {cart.length}</p>;
                  }
                  
                  export default function App() {
                    const cart = [{ name: "Sencha" }, { name: "Assam" }];
                    return (
                      <>
                        <CartBadge />
                        <CartContext value={cart}>
                          <p>Teas go here.</p>
                        </CartContext>
                      </>
                    );
                  }

                  Choose an answer to see the explanation.

                4. Question 4 of 7What makes useCart a custom hook?

                  Choose an answer to see the explanation.

                5. Question 5 of 7Which line is the React 19 way to provide the cart, the one to use in new code?

                  Choose an answer to see the explanation.

                6. Question 6 of 7With two contexts and the CartProvider from the lesson, you click Add Sencha. Which components render again?

                  Choose an answer to see the explanation.

                7. Question 7 of 7After the reducer moves into CartProvider, where is the cart kept?

                  Choose an answer to see the explanation.

                Key takeaways

                • Prop drilling passes state and handlers through layers that don't use them. Context lets any component below a provider read a value directly.
                • To combine a reducer with context: create two contexts, provide the state and dispatch from the component that calls useReducer, and read them with useContext anywhere below.
                • A provider component like CartProvider holds the reducer and both providers, and renders its children inside them.
                • Custom hooks like useCart and useCartDispatch hide which context a component reads, and can throw a clear error when there's no provider.
                • dispatch never changes, so a separate context for it lets components that only send actions skip the renders that cart changes cause.
                • Since React 19, you write <CartContext value={cart}> instead of <CartContext.Provider>.

                Remember the one-liner.
                One provider holds the reducer; the state goes down through one context, actions come up through dispatch in another.

                Coming next: Thinking in React. It builds a searchable product table from a design, in five steps, and decides what is state and where each piece lives.

                CompleteFrontend Clear concepts. Working examples.