Bonjour!

L31 React · lecture 6 · Le Bistro track 5 of 5

Le
passe

The pass is the counter between the kitchen and the room. Plates are put there once, and every waiter picks up what they need. Nobody hands plates from person to person.

Theme
Context, useReducer, compound components
Le Bistro today
One cart, one session, an FAQ
Your project
v0.5 · context and a reducer
Length
2 hours · one break
le passe“one place for everyone”

Route Lecture 5 of 5

Le Bistro, lecture by lecture

L27 · done

Components

Home page and menu from components and a data file.

L28 · done

State

Add to cart works. Chips filter, search finds.

L29 · done

Effects

The real menu from the API. A cart that survives a refresh.

L30 · done

Routing

Pages with their own address. Login and staff-only doors.

L31 · today

Context

One cart and one session for the whole site.

Your project v0.1 ✓catalog from a data file v0.2 ✓cart, filters, search v0.3 ✓real API, saved cart v0.4 ✓pages, login, guards v0.5 · todaycontext and a reducer

Today's goal Press next to unveil it

The same site, with half the wiring

Under the blind: App.tsx at the end of today. It was 87 lines.

  • 39 lines, and not a single cart or session prop
  • Every component asks for what it needs
  • All the cart's rules in one function
  • A new FAQ page from a reusable accordion

Apéritif Before we start

Questions from last time?

Routes, layouts, login, guards.

01

What does <Outlet /> do?

It marks the place in a layout where the current page appears.

02

Why <Navigate to="/login" replace />?

So Back doesn't return to the locked page, which would redirect to the login again.

03

Does RequireAuth protect the kitchen?

It's a sign on the door. The lock is the API: 401 without a token, 403 with the wrong role.

Homework check Two volunteers, two minutes each

Show us your v0.4

Open a product, copy its link into a new tab. Log in, press F5. Open /profile as a guest.

Look for

  • A product link opens that product
  • F5 on the Vercel link: no 404
  • Still signed in after F5
  • A guest on /profile goes to the login, then back
  • No password in Local Storage

Entrée · Problem 1 Before the new tool

Passed
hand
to hand.

43 props in 8 files, only to move one cart and one session to the few components that need them.

Problem 1 Press next, or click a prop

Follow the props through Le Bistro

43 props carry the cart and the session through 8 files. Pick one.

App
SiteLayout
HomePage
MenuPage
DishPage
RequireAuth
AccountPage
Header
Favourites
DishGrid
DishGrid
CartPanel
DishGrid
DishCard
DishCard
DishCard

Problem 1 Your turn

What would we need?

One place for the session Any component can reach it Nobody in between carries it That is Context

Plat · Solution 1 Press next

Hand to hand, or the pass

Props · hand to hand
Appowns the session
SiteLayoutdoesn't need it, carries it
AccountPageneeds it
Headerneeds it
<AuthProvider> · the pass, around everything
Appno session here
SiteLayoutno props
AccountPageuseAuth() → session
HeaderuseAuth() → session

Solution 1 The whole idea in three lines

Context in three parts

const AuthContext = createContext<AuthContextValue | null>(null); <AuthContext value={{ session, login, logout }}>{children}</AuthContext> const auth = useContext(AuthContext);
01 · build the pass

createContext makes a place for one kind of value. null is what you get outside any provider.

02 · put the value on it

The provider: every component inside it can reach the value. React 19 writes <AuthContext>; older code writes <AuthContext.Provider>.

03 · pick it up

useContext in any component inside, at any depth. When the value changes, that component re-renders.

Solution 1 · Recipe Type it with me

The pass for the session: AuthContext.ts

  1. What is on the passThe session, and the two ways to change it.src/auth/AuthContext.ts
  2. Build the passnull means "no provider above me".
  3. A hook to pick it upIt fails loudly if someone forgot the provider.

Solution 1 · Recipe The session moves out of App

Put the session on the pass: AuthProvider.tsx

  1. The session moves hereCut the line from App, paste it here. Same key: nobody gets signed out.src/auth/AuthProvider.tsx
  2. The valueThe session and the two actions.
  3. Wrap the childrenWhatever is inside gets the value.
  4. Wrap the whole appInside BrowserRouter, around App.src/main.tsx

Solution 1 · Recipe Now delete props

Read the session anywhere

  1. Headersession and onLogout leave its props, and SiteLayout's.Header.tsx
  2. The guard and the loginOnly roles is left on RequireAuth.RequireAuth.tsx, LoginPage.tsx
  3. The pagesAccountPage and KitchenPage take session and logout from the hook.
  4. App lets goNo session state, no session props. TypeScript underlines every leftover.App.tsx

Solution 1 Click the buttons, watch the counters

Who re-renders when the value changes?

<AuthContext>session
<CartContext>cart
Headerreads: session, count0
AccountPagereads: session0
Footerreads: nothing0
DishCardreads: cart · Coq au Vin0
DishCardreads: cart · Crème Brûlée0
DishCardreads: cart · Bordeaux0

The rule

Every component that reads a context re-renders when its value changes. Keep contexts small and split them by topic.

Solution 1 Ask in this order

Is context the right tool?

Do components far apart need it?
Propsor lift the state to the closest parent (L28)
Does it change many times a second?
Keep it localor a store with selectors (L32)
Is it data from a server?
TanStack Querycaching, refetching (L34)
Context ✓session, cart, theme, language

Solution 1 The errors you will see today

When context goes wrong

A reader outside the provider

<App /> in main.tsx without <AuthProvider>
Uncaught Error: useAuth() must be used inside <AuthProvider>
<AuthProvider><App /></AuthProvider>

Context and provider in one .tsx file

export const AuthContext = … and export function AuthProvider in one file
Fast refresh only works when a file only exports components. Move your React context(s) to a separate file.
AuthContext.ts (context + hook), AuthProvider.tsx (component)

Everything in one context

<AppContext value={{ session, cart, theme, … }}>
Every reader re-renders on every change.
One context per topic: AuthContext, CartContext

Local state in context

isCartOpen in a context
Only the Header uses it, but everyone can change it.
const [isCartOpen, setIsCartOpen] = useState(false); in Header

Quick check The pass

Which component can call useAuth()?

The provider covers everything rendered inside it, however deep. Importing the context is not enough: outside the provider you get null, and our hook throws.

Atelier · Classwork 1 15 minutes, on your own project

Your session, everywhere

Baseeveryone

An auth context

  • auth/AuthContext.ts: the type, createContext, useAuth().
  • auth/AuthProvider.tsx: the session from useLocalStorage, login, logout. Wrap the app in main.tsx.
  • Header, LoginPage, ProfilePage and RequireAuth call useAuth(). Delete the session props.
Plusmost of you

A theme

  • A ThemeContext with "light" | "dark" and a toggle in the Header.
Profast finishers

A theme that remembers

  • It survives F5 and starts from the system setting (prefers-color-scheme).

Done when

  • Searching the project for session= finds nothing
  • Login, F5: still signed in
  • Sign out from the Header works
  • The guard still sends guests to /login

Stuck?

export function AuthProvider({ children }: { children: ReactNode }) {
  const [session, setSession] = useLocalStorage<Session | null>("shop.session", null);
  const value = { session, login: setSession, logout: () => setSession(null) };
  return <AuthContext value={value}>{children}</AuthContext>;
}

Pause café 10 minutes

Pause café

After the break: too many cooks in the cart.

Second plat · Problem 2 A new wish from the chef

Too many
cooks.

"Add a button that orders the usual: three dishes at once." Simple? The cart's rules are spread over four files, and the new button knows none of them.

Problem 2 Add Coq au Vin, then press "Order the usual"

The new button breaks the cart

Le BistroYour order · 0

Your order

Problem 2 Every cook has a different recipe

Where do the cart's rules live?

App.tsx

"remove a line at 0"

changeQty filters out lines with 0. addToCart just appends.

DishCard.tsx

"no duplicate lines"

Only because the card hides "Add to cart" once the dish is in the cart.

QuantityStepper.tsx

"never more than 10"

Only because the stepper disables its + button.

DishCard.tsx

"nothing off the menu"

Only because the card disables the button.

All the rules in one place Buttons say what happened, not how to change the cart Rules you can test without a screen useReducer

Solution 2 How a kitchen takes orders

The ticket rail: useReducer

DishCard"Add to cart"
QuantityStepper"+"
Favourites"Order all three"
dispatch(ticket){ type: "added", dish }
cartReducer(cart, ticket)all the rules, one function
the new cartevery reader re-renders
ticket

The deal

Components only describe what happened. One pure function decides what the new cart is. That function is the only cook.

Solution 2 A pure function

A reducer in four parts

function cartReducer(cart, action) { switch (action.type) { case "added": … case "removed": … } return newCart; } const [cart, dispatch] = useReducer(cartReducer, []);
01 · the state now

The cart as it is before this ticket.

02 · what happened

An object with a type: { type: "added", dish }.

03 · one case per ticket

The rules of each kind of change.

04 · a new state

A new array, never a changed old one. No fetch, no localStorage inside.

Solution 2 Send tickets, watch the chef

Run the tickets through the reducer

cartReducer(cart, ticket)Bouillabaisse is off the menu today

Solution 2 · Recipe Type it with me, part 1 of 2

The tickets and the first rule: cartReducer.ts

  1. Name every changeOne type per ticket. TypeScript rejects a typo like "add".src/cart/cartReducer.ts
  2. One rule, one placeThe stepper imports MAX_QTY from here.
  3. The chefCart and ticket in, new cart out. A switch on the type.
  4. "added"Off the menu? Nothing changes. Already there? One more, up to 10. Otherwise a new line.

Solution 2 · Recipe Part 2 of 2

The other three tickets

  1. "qtyChanged"+1 or −1, never above 10, and gone at 0.
  2. "removed" and "cleared"The ✕ on a line, "Empty the order".
  3. Try it without a screenIt's a plain function: call it in the console or a test.

Solution 2 · Recipe The same three parts as before the break

The pass for the cart: CartContext.ts

  1. What the pass offersThe lines, what we calculate from them, and named actions. Components never see dispatch.src/cart/CartContext.ts
  2. Build the passExactly like AuthContext.
  3. The hookuseCart(), loud when the provider is missing.

Solution 2 · Recipe The reducer moves in, part 1 of 2

The cart provider: state and saving

  1. Read the saved cart onceA plain function for the first state.src/cart/CartProvider.tsx
  2. useReducerThe reducer, a start value, and loadCart to compute it lazily, only on the first render.
  3. Save after every changelocalStorage is the outside world: an effect, never inside the reducer.

Solution 2 · Recipe Part 2 of 2

The cart provider: what goes on the pass

  1. The data, and what we calculate from itcount, total and qtyOf are derived on every render: never stored.
  2. Named actionsEach one writes a ticket.
  3. On the passSame as the auth provider.
  4. Wrap the appBoth providers around App.src/main.tsx

Solution 2 · Recipe The great prop deletion

Read the cart anywhere

  1. DishCard asks the cart itselfIts props shrink to dish.DishCard.tsx
  2. Header and CartPanelThe count, the lines, the total, and a new "Empty the order".Header.tsx, CartPanel.tsx
  3. The new button"Order all three" sends three tickets. No duplicates.Favourites.tsx
  4. Delete, delete, deleteDishGrid, Favourites, the pages, SiteLayout, App: no cart props left. App: 87 → 39 lines.

Solution 2 Context in the small: click the questions

Parts that work together: the FAQ

Like <select> and <option>: parts that only make sense together, sharing state through a small private context.

Solution 2 · Recipe Copy the file, read these parts

Inside Accordion.tsx

  1. Two small contextsNot exported: only the parts can use them.src/components/Accordion.tsx
  2. Accordion owns the stateWhich item is open. Click the open one again to close it.
  3. AccordionItem tells its parts who they are<ItemContext value={{ value, isOpen, panelId }}> around its children; panelId comes from useId().
  4. Trigger and Panel only readThe panel hides itself when its item is closed.

Solution 2 The bugs of the second half

When a reducer goes wrong

Changing the old cart

cart.push({ dish, qty: 1 }); return cart;
Nothing re-renders: it's the same array, so React sees no change.
return [...cart, { dish, qty: 1 }];

A forgotten return

case "removed": cart.filter((item) => …);
error TS7029: Fallthrough case in switch. (Without the check: "removed" runs into "cleared" and empties the cart.)
case "removed": return cart.filter((item) => …);

A typo in the ticket

dispatch({ type: "add", dish })
Type '"add"' is not assignable to type '"added" | "qtyChanged" | "removed" | "cleared"'.
dispatch({ type: "added", dish })

Side effects in the reducer

localStorage.setItem("bistro.cart", …) inside the reducer
StrictMode runs reducers twice in development. A reducer must only calculate.
useEffect(() => save(items), [items]) in the provider

Solution 2 Le Bistro, version 0.5

The whole picture

<StrictMode>
<BrowserRouter>the address
<AuthProvider>the session
<CartProvider>the cart + reducer
<App />routes only
…/menu
The menu with the cart panel open
…/faq
The FAQ page

Full code: bistro-track/code/after-L31. The Le Bistro track is complete; from L32 on, the course builds on this shape.

Quick check The chef's rules

Which line may go inside a reducer?

A reducer only calculates a new state from the old state and the action. Saving and fetching are side effects (an effect or an event handler). Pushing changes the old array, so React sees no change.

Atelier · Classwork 2 · with AI 15 minutes, on your own project

Prompt it: one cart, one reducer

The taskthe AI writes, you check
  • BaseA cartReducer (added, qtyChanged, removed, cleared: no duplicates, never more than stock, gone at 0), used in App with useReducer.
  • PlusA CartProvider + useCart(): no cart props left.
  • ProA compound Accordion on the product page.

Your prompt, in five parts (CRAFT)

C

ContextYour shop and stack. Paste the cart code from App.tsx and types.ts.

R

Role"A React teacher who writes pure, simple reducers."

A

ActionThe Base task with your rules, then Plus in a follow-up prompt.

F

Format"Context and hook in a .ts file, the provider in .tsx. Only changed files."

T

Target"Mark every rule in the reducer with a one-line comment."

Done when

  • The same product twice: one line, qty 2
  • Never more than in stock
  • No push, splice or localStorage in the reducer
  • AI.md: your prompt and one thing you changed

A sample prompt · Furniture house

CONTEXT
My shop "Maison" sells furniture. Vite + React 19 +
TypeScript + Tailwind 4. My cart is useState in
App.tsx (pasted, with types.ts).
ROLE
A React teacher who writes pure, simple reducers.
ACTION
Replace it with cartReducer in src/cart/cartReducer.ts:
added, qtyChanged, removed, cleared. No duplicate
lines, never more than product.stock, gone at 0.
FORMAT
Only the files that change, complete, with their paths.
TARGET
Mark every rule in the reducer with a one-line comment.

Masterclass · LAB-31 Press next: the prompt opens part by part

Le Bistro so far, in one prompt

Prompt · LAB-31 · Le Bistro after L31
C

Contextwhat exists and what we use

Le Bistro, a French restaurant site. Vite + React 19 + TypeScript + Tailwind CSS 4,
react-router 8 (import from "react-router"). API at /api through a Vite proxy:
dishes, categories, search; POST /api/v1/auth/login { email, password } ->
accessToken (lives 2 minutes); GET /api/v1/auth/me -> the role.
R

Rolewho answers

You are a senior React developer who writes code for beginners to read.
A

Actionwhat to build, step by step

Build the site as it is so far (real menu, saved cart, search, pages, login,
RequireAuth with roles), with no prop drilling:
- AuthContext + AuthProvider + useAuth() for the session, login and logout.
- cartReducer (added, qtyChanged, removed, cleared: no duplicates, max 10,
  nothing off the menu) in a CartProvider + useCart(), saved to localStorage.
- "Order all three" on the favourites, "Empty the order" in the cart panel.
- An FAQ page from a compound Accordion (Accordion, AccordionItem,
  AccordionTrigger, AccordionPanel sharing a small context).
F

Formatwhat the answer looks like

The provider tree first, then every file in full with its path.
Contexts and hooks in .ts files, providers in .tsx files.
T

Targetwho reads it, when it's done

Beginners will read it: a pure reducer, App.tsx only lists routes.
It must pass npm run build.

Masterclass · LAB-31 The AI writes fast. You decide if it's right.

Run it, then check it

  1. Copy the prompt into an AI toolClaude, ChatGPT, Copilot Chat: any of them; nothing to attach: the data comes from the API
  2. Build what it gives backa new Vite project, every file pasted at its path
  3. Run npm run dev and npm run buildan error message goes straight back into the chat
  4. Compare with code/after-L31the version we built by hand

Check it with what you learned today

  • The reducer returns new arrays and never touches localStorage
  • Two contexts (auth and cart), not one for everything
  • useAuth() and useCart() throw outside their provider
  • No cart= or session= props left
  • Contexts and hooks in .ts, providers in .tsx

AIs often save to localStorage inside the reducer, or put the whole app in one context. Both are on today's mistakes slides.

Masterclass · LAB-31 The same five parts, without the labels

The short version

Prompt · LAB-31 · the short version

You're a senior React developer writing for beginners. Le Bistro is a Vite + React 19 + TypeScript + Tailwind 4 app with react-router 8 and an API at /api through a Vite proxy (dishes, categories, search, a 2-minute token login, /auth/me for the role). Build the real menu, saved cart, search, pages, login and role guards with no prop drilling: AuthProvider with useAuth(), a pure cartReducer (no duplicates, max 10, nothing off the menu) in a CartProvider with useCart() saved to localStorage, "Order all three" and "Empty the order", and an FAQ page from a compound Accordion. Show the provider tree, then every file in full with its path, with contexts and hooks in .ts files and providers in .tsx files. Keep App.tsx to the routes only, and make sure npm run build passes.

ContextRoleActionFormatTarget

How CRAFT works

The five parts are a checklist for your thinking, not a form to fill in. Once all five are there, the labels can go: the prompt reads like a message to a colleague.

Dessert What we solved today

Two problems, two tools, one pattern

Problem 1

The cart and the session passed hand to hand: 43 props in 8 files.

Context

createContext, a provider around the app, useAuth() / useCart() anywhere inside.

Problem 2

The cart's rules spread over four files; a new button broke them.

useReducer

Tickets describe what happened; one pure function holds every rule.

Bonus

A component made of parts: the FAQ accordion.

Compound components

The parts share state through a small private context.

Dessert Click a card to check yourself

What does this do?

01
01

Builds the pass. Outside any provider, readers get null.

02
02

The provider: everything inside it can read value.

03
03

Reads the cart from context. Re-renders when it changes.

04
04

State managed by a reducer. loadCart computes the first state once.

05
05

Puts a ticket on the rail. The reducer decides the new cart.

06
06

One part of a compound component. Its Trigger and Panel know it.

Homework Before L32

Your shop, version 0.5

Live link required
Baserequired

Context and a reducer

  • AuthProvider + useAuth(): no session props anywhere.
  • CartProvider with useReducer + useCart(): no cart props anywhere.
  • The reducer holds every rule: no duplicates, never more than stock, gone at 0, cleared.
  • The cart survives F5; "Empty the cart" works.
Plusrecommended

A compound component

  • An Accordion (or Tabs) built from parts with a small context. Use it on the product page or an FAQ page.

Before you submit

  • Search for cart= and session=: nothing
  • The same product twice: one line
  • The reducer has no side effects
  • npm run build passes

Full brief and grading: homework.md.

Le Bistro track Complete

Five lectures, one restaurant

L27

Components

JSX, props, lists and keys.

L28

State

useState, events, lifting state, derived values.

L29

Effects

useEffect, fetching, custom hooks, useRef.

L30

Routing

Pages, params, login, guards.

L31

Context

Context, useReducer, compound components.

Your project v0.1 – v0.5 ✓a real shop, deployed L32 – L41Zustand, forms, TanStack Query, tests, Next.js L42 · Demo Daypresent it to the class

L31 Le passe

Merci!

Questions now, or in the class chat. See you at L32.

Next time · L32

A store without a provider.

Our CartProvider is 40 lines, and every reader re-renders on every change. Zustand does the same in a few lines, and lets a component subscribe to just the part it needs. Plus the big question: where should each piece of state live?