Bonjour!

L29 React · lecture 4 · Le Bistro track 3 of 5

Le
marché

Effects and real data. Le Bistro's motto is "market first": the menu bends to what the morning market has. Today our menu comes from the real kitchen server.

Theme
useEffect, useRef, custom hooks
Le Bistro today
The real menu from the API
Your project
v0.3 · DummyJSON, saved cart
Length
2 hours · one break
le marché“market first”

Route Lecture 3 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 · today

Effects

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

L30

Routing

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

L31

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 · todayreal API, saved cart v0.4pages, login, guards v0.5context and reducer

Today's goal Press next to unveil it

From a file to the kitchen's server

Left: 12 dishes we typed. Under the curtain: the real menu, a search that asks the server, and grey cards while the kitchen answers.

  • 25 real dishes, live from the API
  • Loading, error and empty states
  • A search with a delay, and a cart that survives F5
Le Bistro after L28After L28
Le Bistro after L29After today

Apéritif Before we start

Questions from last time?

useState, lifting state, new arrays, derived values.

01

Why [...cart, item] and not cart.push(item)?

React compares old and new state with ===. A pushed array is the same array, so React sees no change.

02

Where does state live when two components need it?

In their closest common parent. Data goes down as props, events come up as functions.

03

Should the cart total be state?

No. It is derived from the cart: calculate it during render.

Homework check Two volunteers, two minutes each

Show us your v0.2

Add two products, change a quantity in the cart panel, filter by a category. Then refresh the page.

Look for

  • The cart lives in App
  • New arrays: no push on state
  • Count and total are calculated
  • The filtered list is calculated
  • The same product shows the same quantity everywhere

Entrée · Problem 1 Before the new tool

The menu is a file.
The kitchen is a server.

The chef changes a price at 11:00. Our site still shows yesterday's, because we typed it.

Problem 1 What we have, what is real

Our file and the real menu

src/data/dishes.ts

  • 12 dishes, typed by hand
  • Prices from the day we copied them
  • Bouillabaisse "off the menu" forever
  • Changes need a new deploy
vs

GET /api/v1/enterprise/dishes

  • Every dish the kitchen serves today
  • Today's prices, today's availability
  • Filters on the server: ?category=drinks
  • A search: /api/v1/enterprise/search?q=

Open it in the browser: srmsapi.stepacademy.ge/swagger

It's the same JSON shape as our Dish type. Only the source changes.

Problem 1 The first idea, and why it explodes

Fetch right inside the component?

0 requests
ConsoleNetwork

render → fetch → setDishes → render → fetch …

Every render starts a request. Every answer triggers a render. It never stops.

Problem 1 Your turn

What would we need?

Run code after the render, not during it Only once, or when something changes Clean up when we leave That is an effect

Plat · Solution 1 The tool

useEffect: code that runs after the render

useEffect(() => { // talk to the outside world return () => { /* clean up */ }; }, [dependencies]);
01 · the effect

Runs after React has put the render on the screen. Fetch, timers, listeners, localStorage live here.

02 · the cleanup

Optional. Runs before the effect runs again, and when the component leaves the page.

03 · the dependencies

When to run it again: after any render in which one of these values changed.

Solution 1 Press next, one moment at a time

The life of an effect

Appears (mount) category changes Leaves (unmount)
renderReact calls MenuSection()
commitskeleton on screen
effect runsfetch drinks…
render + commitnew category on screen
cleanup (old)cancel the drinks request
effect (new)fetch desserts…
cleanupcancel what's left

Cleanup always runs before the next effect, and once at the end.

Solution 1 When does the effect run again?

The dependency array decides

You writeIt runsUse it for
useEffect(fn, [])once, after the first rendera keyboard listener, a page view
useEffect(fn, [category])after the first render, and whenever category changesfetching the dishes of a category
useEffect(fn)after every renderalmost never. With a setState inside: an endless loop

The rule: list every value from the component that the effect uses.

The linter (eslint-plugin-react-hooks) checks it for you: "React Hook useEffect has a missing dependency".

Solution 1 The first real request fails

CORS: the server only talks to friends

Browserlocalhost:5173
Le Bistro APIsrmsapi.stepacademy.ge
Vite dev serverthe same address as the page
Console
Access to fetch at 'https://srmsapi.stepacademy.ge/api/v1/…' from origin 'http://localhost:5173' has been blocked by CORS policy

Now fetch the relative path: fetch("/api/v1/enterprise/dishes"). Restart npm run dev after editing the config.

Solution 1 · Recipe Type it with me

The favourites come from the API

  1. State for the answernull means "no answer yet". A second state for failure.Favourites.tsx
  2. The effect fetches onceEmpty dependency array: after the first render.
  3. Check res.okfetch does not fail on 404 or 500. We throw ourselves.
  4. Save the data or the failuresetState inside then and catch: after the answer, never during render.
  5. Three endings in the JSXError, loading (skeleton), data.

Solution 1 Design all of them, not only the happy one

Every request has four endings

Loading
dishes === null

Grey cards: the page keeps its shape while we wait.

Data
dishes.length > 0

The real menu.

Empty

Nothing on the menu matches “pizza”.

dishes.length === 0

Not an error: a valid answer with nothing in it.

Error

We couldn't load the menu — please try again.

failed === true

No internet, a 500, a typo in the URL.

ConsoleNetwork
GET/api/v1/enterprise/dishes(canceled)2 ms
GET/api/v1/enterprise/dishes200512 ms
1 · effect runsReact mounts the component
2 · cleanupReact pretends the component left
3 · effect runs againand mounts it back

Solution 1 Don't panic

"Why do I see two requests?"

In development, <StrictMode> runs every effect twice on purpose: effect, cleanup, effect.

It's a test, not a bug

If your effect has a correct cleanup, the user can't tell the difference. In production (npm run build) it runs once.

Solution 1 The errors you will see today

When effects go wrong

An async effect function

useEffect(async () => { … }, []);
Effect callbacks are synchronous to prevent race conditions. Put the async function inside.
useEffect(() => { fetch(…).then(…); }, []);

A missing dependency

useEffect(() => { load(category); }, []);
React Hook useEffect has a missing dependency: 'category'.
useEffect(() => { load(category); }, [category]);

An object created during render as a dependency

const filter = { category }; useEffect(…, [filter]); // new object every render
The effect runs after every render: a loop of requests.
useEffect(…, [category]); // a string compares by value

Trusting fetch with errors

fetch(url).then((res) => res.json())
A 404 or 500 still "succeeds": res.json() then fails with an unclear error.
if (!res.ok) throw new Error(`HTTP ${res.status}`);

Quick check The dependency array

This effect loads the dishes of the selected category. Which array?

The effect uses category, so it must run again when category changes. [] loads only the first category. [dishes] loops (the effect changes dishes). No array runs after every render: a loop too.

Atelier · Classwork 1 15 minutes, on your own project

Real products

Baseeveryone

Fetch your category

  • fetch("https://dummyjson.com/products/category/your-category") in a useEffect with [].
  • The answer is { products: Product[] }: save data.products.
  • A loading text while products === null, an error message in catch.
Plusmost of you

A skeleton

  • Grey placeholder cards instead of the loading text (animate-pulse).
Profast finishers

Categories from the API

  • Your chips come from https://dummyjson.com/products/categories, filtered to your theme.

Done when

  • Your data file is no longer imported
  • DevTools → Network shows the request
  • Offline mode shows your error message
  • No endless requests

Stuck?

useEffect(() => {
  fetch(URL)
    .then((res) => res.json())
    .then((data) => setProducts(data.products))
    .catch(() => setFailed(true));
}, []);

Pause café 10 minutes

Pause café

After the break: we click faster than the kitchen cooks.

Second plat · Problem 2 The real world is slow and busy

Faster than
the kitchen.

Main courses take the API 3 seconds, drinks 0.2 seconds. Guests click faster than that.

Problem 2 Try it: click "Main Courses", then quickly "Drinks"

The slow answer wins

Click a chip.

Delays are the real API's: main courses 3 s, drinks 0.2 s, others 0.5 s.

Problem 2 Two more pains

Ten letters, ten requests. Then 429.

0 requests this minute (the API allows 10)

And refresh the page…

…the cart is empty. State lives in memory. F5 wipes the memory.

ConsoleNetwork

Problem 2 Your turn

What would we need now?

Cancel the old request Wait until typing stops Save the cart somewhere that survives F5 Write it once, reuse it Cleanup and custom hooks

Solution 2 · Recipe The cleanup cancels, the tag protects

Cancel the old request

  1. One AbortController per runA remote control for one fetch.
  2. Give fetch its signalSo the controller can stop it.
  3. The cleanup abortsRuns before the next effect: the old request dies.
  4. Ignore our own cancelAn aborted fetch rejects. That's not an error to show.
  5. Tag the answer with its requestShow data only if it belongs to the current path. loading is derived.
MenuSection.tsx
hooks/useApi.ts
useState(category) · useState(query)
useState(result)
useEffect(fetch, abort)
loading / error / data
return ( … JSX … )
export function useApi(path)a function that uses hooks
const menu = useApi(menuPath)one line instead of twenty
Favourites, DishPage, Account…every component can reuse it

Solution 2 Custom hooks

Move the logic into a hook

1 · Move

The state, the effect and the derived values move out of the component.

2 · Call it

A custom hook is a function whose name starts with use and that calls other hooks.

3 · Reuse it

Each component that calls it gets its own state. Hooks share logic, not data.

Solution 2 · Recipe Copy or type: this file is the heart of today

The useApi hook

  1. A function named use…
  2. One state: the last answer
  3. The effect from the last slide
  4. Return derived values
  5. Use it

Solution 2 · Recipe One line per request

MenuSection asks the API

  1. The path depends on the chip"all" → every dish, otherwise ?category=.MenuSection.tsx
  2. Two requests, two linesCategories for the chips, dishes for the grid.
  3. The four endingsSkeleton, error, empty, grid.
  4. Delete the data filesNo more data/dishes.ts, data/categories.ts.

Try the race again: Main Courses, then Drinks.

The grid stays on drinks. The cancelled request shows as "(canceled)" in DevTools.

Solution 2 · Recipe A second custom hook

A cart that survives F5: useLocalStorage

useState

cart: []

useLocalStorage

cart: []

Solution 2 One request instead of ten

Debounce: wait until typing stops

c
o
q
each key starts a 400 ms timer and cancels the previous one
⏱ 400 ms of silence → GET /search?q=coq

Same idea as the race: a cleanup cancels the old timer.

Solution 2 · Recipe A third, tiny hook

useDebounce and the search

  1. The hookA timer in an effect. The cleanup clears it.src/hooks/useDebounce.ts
  2. Debounce the queryThe input stays instant. The request waits.MenuSection.tsx
  3. Search or menu, never bothPass null to the request we don't need.
  4. The 429 messageThe API says when to try again: retryAfter.

Solution 2 The last hook of the day

useRef: press "/" to search

  1. A ref is a box React doesn't watchChange ref.current and nothing re-renders. Here it holds the real <input>.SearchBox.tsx
  2. Attach itref={inputRef}: React puts the DOM element in the box.
  3. Listen to the keyboardA listener on window is the outside world: an effect.
  4. Remove it in the cleanupOr every StrictMode remount adds a second listener.

Solution 2 The most important warning of today

You might not need an effect

An effect to calculate

Two renders instead of one, a second copy of the data, and a lint error: "set-state-in-effect".

Just calculate it

Derived value, from L28. One render, one truth.

Effects are for the outside world: the network, timers, the browser, the keyboard.

If you only transform props or state, you don't need one.

localhost:5173/#menu
Le Bistro search results from the API

Solution 2 Le Bistro, version 0.3

The page at the end of today

  • The menu and the chips come from the API
  • Skeleton, error, empty and data states
  • Old requests are cancelled
  • The search waits 400 ms
  • The cart survives F5

Full code: bistro-track/code/after-L29.

Solution 2 The bugs of the second half

When hooks go wrong

A listener without cleanup

useEffect(() => { window.addEventListener("keydown", f); }, []);
Two listeners after StrictMode, more after every remount. "/" focuses twice.
return () => window.removeEventListener("keydown", f);

A hook inside an if or a loop

if (searching) { const results = useApi(path); }
React Hook "useApi" is called conditionally.
const results = useApi(searching ? path : null);

A timer that is never cleared

useEffect(() => { setTimeout(…, 400); }, [value]);
Every keystroke still fires: no debounce at all.
const t = setTimeout(…); return () => clearTimeout(t);

Reading a ref while rendering

return <p>{inputRef.current?.value}</p>;
Shows stale values: refs don't trigger renders.
Use state for anything the screen shows.

Quick check Effect or not?

Which one needs useEffect?

A, B and D are calculations: derive them during render. localStorage is outside React: writing to it is a side effect.

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

Prompt it: search and a saved cart

The taskthe AI writes, you check
  • BaseThe cart survives F5 with a useLocalStorage(key, initial) hook.
  • PlusA debounced search with dummyjson.com/products/search?q= and an AbortController cleanup.
  • ProYour own useApi(url) hook for the products and the categories.

Your prompt, in five parts (CRAFT)

C

ContextYour shop and stack. DummyJSON allows any origin: no proxy. Paste App.tsx and your search box.

R

Role"A React teacher who explains every effect."

A

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

F

Format"Only the files that change, complete, with their paths. Hooks in src/hooks/."

T

Target"Point out every cleanup and the reason it's there."

Done when

  • F5 keeps the cart
  • Typing six letters sends one request
  • Every effect that starts something cleans it up
  • AI.md: your prompt and one thing you changed

A sample prompt · Tech store

CONTEXT
My shop "Circuit" sells smartphones, laptops and
tablets. Vite + React 19 + TypeScript + Tailwind 4.
DummyJSON allows any origin. App.tsx: (pasted)
ROLE
A React teacher who explains every effect.
ACTION
Keep the cart after F5 with a useLocalStorage(key,
initial) hook in src/hooks/useLocalStorage.ts, and use
it in App for the cart.
FORMAT
Only the files that change, complete, with their paths.
TARGET
I'm learning effects: point out every cleanup and why
it's there.

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

Le Bistro so far, in one prompt

Prompt · LAB-29 · Le Bistro after L29
C

Contextwhat exists and what we use

Le Bistro, a French restaurant site. Vite + React 19 + TypeScript + Tailwind CSS 4.
API: https://srmsapi.stepacademy.ge. It only accepts its own site, so call
/api/... through a Vite proxy. GET /api/v1/enterprise/dishes, /categories,
/search?q= (10 requests a minute, then 429 with Retry-After).
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 home page and the menu with a working cart, now on real data:
- A useApi(path) hook: loading, error and empty states, an AbortController in
  the cleanup, every answer tagged with its path.
- Grey skeleton cards while loading. Categories and dishes from the API.
- A search debounced by 400 ms (useDebounce), with a friendly 429 message.
- The cart survives F5: useLocalStorage("bistro.cart"). "/" focuses the search.
F

Formatwhat the answer looks like

The file tree first, then every file in full with its path, vite.config.ts too.
T

Targetwho reads it, when it's done

Students learning effects: no setState during render, complete dependency
arrays, one comment per cleanup. It must pass npm run build.

Masterclass · LAB-29 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-L29the version we built by hand

Check it with what you learned today

  • Every fetch is in an effect, with a cleanup that aborts it
  • The dependency arrays list everything the effect uses
  • No useEffect(async () => …)
  • Relative /api/... paths and the proxy, no absolute API address
  • The search waits 400 ms and handles 429

AIs often forget the cleanup, so a slow answer overwrites a newer one. You saw that race today.

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

The short version

Prompt · LAB-29 · the short version

You're a senior React developer writing for beginners who are learning effects. Build Le Bistro as a Vite + React 19 + TypeScript + Tailwind 4 app on the real API at https://srmsapi.stepacademy.ge, called as /api/... through a Vite proxy (dishes, categories, and a search limited to 10 requests a minute). Make the menu, the cart and the search work with a useApi(path) hook (loading, error, empty, AbortController cleanup, answers tagged by path), skeleton cards, a 400 ms debounced search with a kind 429 message, a cart saved with useLocalStorage("bistro.cart"), and "/" to focus the search. Show the file tree, then every file in full with its path, vite.config.ts included. Keep the dependency arrays complete, never set state during render, comment each cleanup, 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

Problem 1

Our menu was a file. Fetching during render looped forever.

useEffect

Talk to the outside world after the render. Dependencies say when to run again. Design loading, error and empty.

Problem 2

Slow answers won the race, every keystroke was a request, F5 emptied the cart.

Cleanup and custom hooks

Cleanups cancel old work. useApi, useDebounce, useLocalStorage package it for reuse. useRef holds what React doesn't watch.

Dessert Click a card to check yourself

What does this do?

01
01

Runs fn once, after the first render.

02
02

A cleanup: cancels the old request before the next effect.

03
03

Turns a 404 or 500 into an error we can show.

04
04

The query, but only after 400 ms without typing.

05
05

A box that survives renders and doesn't cause them. Often holds a DOM element.

06
06

A custom hook: reusable logic. Each caller gets its own state.

Homework Before L30

Your shop, version 0.3

Live link required
Baserequired

Real data

  • Products come from DummyJSON. The data file is gone.
  • Loading (skeleton), error and empty states.
  • The cart is saved with useLocalStorage.
  • Category chips load products with ?category or the category endpoint, with a cleanup.
Plusrecommended

Search

  • A debounced search with /products/search?q=.
  • "/" focuses the search box (useRef).

Before you submit

  • No missing-dependency warnings
  • Every listener and timer has a cleanup
  • One request per search, not per letter
  • Live on Vercel

Full brief and grading: homework.md.

L29 Le marché

Merci!

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

Next time · L30

“Send me the link to Coq au Vin.”

You can't: everything lives on one page, and the address never changes. Next lecture Le Bistro gets pages, addresses, a login and doors only staff can open.