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”
Le Bistro · L31 · 120 minutes
Today's menu
AccueilHello where Le Bistro is today3′
ApéritifQuestions and two homework demos8′
EntréeProblem 1 passed hand to hand5′
PlatContext one session for the whole site24′
AtelierClasswork 1 your session, everywhere15′
Pause caféBreak music on, screens off10′
Second platProblem 2, useReducer, compound components too many cooks27′
AtelierClasswork 2 with AI prompt it, then check it15′
MasterclassOne prompt, the whole Le Bistro CRAFT, then the short version8′
DessertWrap-up recap, homework, what comes next5′
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 projectv0.1 ✓catalog from a data filev0.2 ✓cart, filters, searchv0.3 ✓real API, saved cartv0.4 ✓pages, login, guardsv0.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 sessionAny component can reach itNobody in between carries itThat is 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).
"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 placeButtons say what happened, not how to change the cartRules you can test without a screenuseReducer
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
Name every changeOne type per ticket. TypeScript rejects a typo like "add".src/cart/cartReducer.ts
One rule, one placeThe stepper imports MAX_QTY from here.
The chefCart and ticket in, new cart out. A switch on the type.
"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
"qtyChanged"+1 or −1, never above 10, and gone at 0.
"removed" and "cleared"The ✕ on a line, "Empty the order".
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
What the pass offersThe lines, what we calculate from them, and named actions. Components never see dispatch.src/cart/CartContext.ts
Build the passExactly like AuthContext.
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
Read the saved cart onceA plain function for the first state.src/cart/CartProvider.tsx
useReducerThe reducer, a start value, and loadCart to compute it lazily, only on the first render.
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
The data, and what we calculate from itcount, total and qtyOf are derived on every render: never stored.
Named actionsEach one writes a ticket.
On the passSame as the auth provider.
Wrap the appBoth providers around App.src/main.tsx
Solution 2 · Recipe The great prop deletion
Read the cart anywhere
DishCard asks the cart itselfIts props shrink to dish.DishCard.tsx
Header and CartPanelThe count, the lines, the total, and a new "Empty the order".Header.tsx, CartPanel.tsx
The new button"Order all three" sends three tickets. No duplicates.Favourites.tsx
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
Two small contextsNot exported: only the parts can use them.src/components/Accordion.tsx
Accordion owns the stateWhich item is open. Click the open one again to close it.
AccordionItem tells its parts who they are<ItemContext value={{ value, isOpen, panelId }}> around its children; panelId comes from useId().
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
…/faq
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
Copy the prompt into an AI toolClaude, ChatGPT, Copilot Chat: any of them; nothing to attach: the data comes from the API
Build what it gives backa new Vite project, every file pasted at its path
Runnpm run dev and npm run buildan error message goes straight back into the chat
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 projectv0.1 – v0.5 ✓a real shop, deployedL32 – L41Zustand, forms, TanStack Query, tests, Next.jsL42 · 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?