The JavaScript library for building user interfaces out of components.
Start from what a web page is and build a to-do app and a recipe browser step by step, from your first component to state, effects, context and data fetching. Then go further: performance, Suspense and transitions, React 19’s Actions and Server Components, accessibility, and how React renders under the hood.
What you’ll be able to do
Set up a React project with Vite and build interfaces from components, props and JSX.
Model state well: minimal, immutable, in the right place, kept or reset on purpose with keys.
Handle forms, effects, refs, reducers, context and data fetching, and know when not to use an effect.
Build accessible components that work with a keyboard and a screen reader.
Use Suspense, transitions and React 19’s Actions, use and form Hooks for smooth async screens.
Explain how React renders, find and fix performance problems, and review React code with confidence.
Modules
How a web page works· 9 slides
Setting up your first project· 9 slides
JSX from scratch· 11 slides
Components· 11 slides
Props: passing data down· 11 slides
Lists and conditions· 10 slides
Events and state· 11 slides
Updating state the right way· 11 slides
Forms and inputs· 10 slides
Sharing state between components· 9 slides
Effects, and when not to use them· 11 slides
Refs and the DOM· 9 slides
Reducers and context· 11 slides
Custom Hooks and fetching data· 10 slides
Accessible components· 10 slides
Performance· 10 slides
Suspense and transitions· 9 slides
Actions, use and the server· 11 slides
Under the hood· 9 slides
Mastery: building and reviewing a large app· 12 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: How a web page works
React1 / 204
Module 1 · How a web page works
A web page is a text file the browser draws
When you open a website, your browser (Chrome, Firefox, Safari, Edge) asks another computer, a server, for a file. The file is plain text written in HTML. The browser reads it and draws what it describes: headings, paragraphs, buttons, pictures.
Every page you visit starts like this.
Three languages work together on almost every page. HTML says what is on the page. CSS says how it looks. JavaScript makes it do things. React is written in JavaScript, so it builds on all three.
Browser
The program that fetches pages and draws them on your screen.
Server
A computer somewhere else that sends files when a browser asks.
HTML
HyperText Markup Language: text that describes the parts of a page.
React2 / 204
Module 1 · How a web page works
HTML describes a page with tags
HTML wraps content in tags. An opening tag like <h1> and a closing tag </h1> mark where something starts and ends. Together they make an element. Elements can sit inside other elements, like boxes in boxes.
WalkthroughInteractive · disabled in the PDF
index.html
1<!doctypehtml>2<html>3<head>4<title>My to-dos</title>5</head>6<body>7<h1>My to-dos</h1>8<ul>9<li>Buy milk</li>10<li>Walk the dog</li>11</ul>12<buttonclass="add">Add a to-do</button>13</body>14</html>
Lines 1: Tells the browser this is a modern HTML page.
Lines 3-5: The head holds facts about the page. title is the text on the browser tab.
Lines 6: The body holds everything you see.
Lines 7:h1 is the main heading.
Lines 8-11:ul is a list, and each li is one item in it.
Lines 12: A button. class="add" is an attribute: extra information on a tag, here a name CSS can use.
React3 / 204
Module 1 · How a web page works
CSS says how things look
CSS is a list of rules. Each rule has a selector (which elements it applies to) and some declarations (what to change). The browser applies every matching rule to every element.
h1 matches every h1 tag. .add matches every element with class="add".
QuizInteractive · disabled in the PDF
You want the to-do list’s text to be bigger. Which language do you change?
You want the to-do list’s text to be bigger. Which language do you change?
React4 / 204
Module 1 · How a web page works
The browser turns HTML into a tree called the DOM
The browser doesn’t keep your HTML as text. It reads it once and builds a tree of objects in memory, one object per element. This tree is the DOM (Document Object Model). What you see on screen is a picture of the DOM, not of the file.
body
h1My to-dos
ul
liBuy milk
liWalk the dog
buttonAdd a to-do
The to-do page from the last slide, as the browser holds it.
Why this matters. Change the DOM and the screen changes straight away. Every interactive page, React or not, works by changing the DOM.
React5 / 204
Module 1 · How a web page works
JavaScript changes the page by changing the DOM
JavaScript is a programming language the browser runs. It can find elements in the DOM, change them, add new ones and listen for things the user does, like clicks. Something the user does is an event.
WalkthroughInteractive · disabled in the PDF
app.js
1const list = document.querySelector('ul')2const button = document.querySelector('.add')34button.addEventListener('click',()=>{5const item = document.createElement('li')6 item.textContent ='A new to-do'7 list.appendChild(item)8})
Lines 1-2:const makes a named variable. querySelector finds the first element matching a CSS selector.
Lines 4:addEventListener says: when this button is clicked, run the function that follows. () => { … } is a function: some code saved to run later.
Lines 5-6: Make a new li element and give it some text.
Lines 7: Put it at the end of the list. The browser draws it at once.
Variable
A name for a value, so you can use it later.
Function
A piece of code saved to run when you need it, often with a name.
Event
Something that happens on the page: a click, a key press, typing.
React6 / 204
Module 1 · How a web page works
Check: what happens on a click
Put in orderInteractive · disabled in the PDF
Someone clicks “Add a to-do” on the page above. Put what happens in order.
1.The DOM has changed, so the browser draws the new item
2.It runs the function you gave to addEventListener
3.The function creates an li and adds it to the ul
4.The browser notices the click on the button
QuizInteractive · disabled in the PDF
After the click, does index.html on the server contain the new to-do?
After the click, does index.html on the server contain the new to-do?
React7 / 204
Module 1 · How a web page works
Changing the DOM by hand gets hard fast
Adding one item is easy. A real to-do app also ticks items off, deletes them, filters them, shows a count and an empty message. Each feature needs code that finds the right elements and changes them, and every change must keep all the others correct.
JavaScript
1functiontick(item){2 item.classList.toggle('done')3// Now update the count…4const left = document.querySelectorAll('li:not(.done)')5 counter.textContent = left.length +' left'6// …and hide it if a filter is on…7// …and show "All done" if nothing is left…8// …and remember to undo all this when it is unticked.9}
Every feature has to know about every other one.
The real problem. The truth lives in two places: your data (the list of to-dos) and the DOM. Keeping them in step by hand is where most bugs come from: a count that is off by one, an item that should be hidden but isn’t.
React8 / 204
Module 1 · How a web page works
React: describe the page, and let it do the changes
React is a JavaScript library (code someone else wrote, for you to use) for building user interfaces. You describe what the page should look like for the data you have. When the data changes, you describe it again, and React works out the smallest set of DOM changes to get there.
By hand
You write how to change the page: find this, add that, remove the other. The order matters and it’s easy to forget a step.
With React
You write what the page is: given these to-dos, show this list. React handles the changes. This style is called declarative.
Your data is the single source of truth. The page always matches it.
You build the page from small reusable pieces called components.
React runs in the browser and changes the DOM for you.
React9 / 204
Module 1 · How a web page works
Recap
HTML describes what is on a page, CSS how it looks, JavaScript what it does.
The browser turns HTML into the DOM, a tree of objects. The screen shows the DOM.
JavaScript makes pages interactive by listening for events and changing the DOM.
Doing that by hand means keeping data and DOM in step yourself, which breeds bugs.
React lets you describe the page for your data, and changes the DOM for you.
FlashcardsInteractive · disabled in the PDF
HTML
The language that describes what is on a page, with tags.
CSS
The language that describes how a page looks, with rules and selectors.
The DOM
The tree of objects the browser builds from HTML. The screen shows it.
Event
Something the user does, like a click, that code can react to.
Declarative
Describing what the result should be, not the steps to get there.
Library
Code someone else wrote, packaged for you to use in your own.
Module 2: Setting up your first project
React10 / 204
Module 2 · Setting up your first project
Three tools come before any React
React code is written on your computer and turned into files a browser understands. For that you need a few tools. You install them once and use them for every project.
Terminal
A window where you type commands instead of clicking. On macOS it is called Terminal; on Windows, PowerShell or Windows Terminal.
Node.js
A program that runs JavaScript outside the browser. The tools that build React projects are written in JavaScript, so they need it.
npm
The Node Package Manager. It comes with Node.js and downloads packages: libraries like React, shared by other developers.
Code editor
A program for writing code, such as VS Code. It colours code and points out mistakes.
Terminal
1node--version2npm--version
Type these in a terminal after installing Node.js from nodejs.org. Each prints a version number.
React11 / 204
Module 2 · Setting up your first project
Vite makes a new project in one command
Vite (French for “quick”, said “veet”) is a build tool: it serves your code to the browser while you work, and packs it into small files when you publish. It can also create a ready-made React project for you.
Lines 1: Create a project called todo-app from Vite’s React and TypeScript template (a starting point).
Lines 2:cd means “change directory”: move into the new project’s folder.
Lines 3: Download the packages the project needs, React among them, into a node_modules folder.
Lines 4: Start the dev server. It prints an address such as http://localhost:5173: open it in your browser.
Try itInteractive · disabled in the PDF
React12 / 204
Module 2 · Setting up your first project
Check: the steps to a running project
Put in orderInteractive · disabled in the PDF
Put the steps in order, from nothing to a page in the browser.
1.Run npm run dev and open the address it prints
2.Install Node.js (it brings npm)
3.Run npm create vite@latest to make the project
4.Move into the project folder with cd
5.Run npm install to download the packages
React13 / 204
Module 2 · Setting up your first project
What is in the new project
todo-app/
node_modules/# downloaded packages: never edit
public/# files served as they are
src/# your code lives here
App.tsx# the first component
main.tsx# starts React
index.css
index.html# the one HTML page
package.json# the project's name, scripts, packages
vite.config.ts# Vite's settings
package.json lists the packages the project depends on and its scripts: short names for longer commands. npm run dev runs the script called dev, which starts Vite.
package.json
1{2"name":"todo-app",3"scripts":{4"dev":"vite",5"build":"tsc -b && vite build"6},7"dependencies":{8"react":"^19.0.0",9"react-dom":"^19.0.0"10}11}
Trimmed. Your version numbers may be newer.
React14 / 204
Module 2 · Setting up your first project
One HTML page, one empty box, and React fills it
A React app usually has a single HTML page with an empty element in it. A small script, main.tsx, finds that element and hands it to React. From then on, React draws everything inside it.
Lines 1-2:import brings in code from a package. react is React itself; react-dom connects it to the browser’s DOM.
Lines 3-4: Load the CSS, and the App component from App.tsx.
Lines 6: Find the empty div. The ! tells TypeScript “trust me, this exists”.
Lines 7-11:createRoot makes React the owner of that div, and render draws App inside it. StrictMode adds extra checks while you develop.
React15 / 204
Module 2 · Setting up your first project
TypeScript: JavaScript with labels
The .tsx files are TypeScript: JavaScript plus types, labels that say what kind of value something is. Your editor checks the labels as you type and underlines mistakes before you ever run the code. The browser never sees them: they are removed when the code is built.
TypeScript
1let title:string='Buy milk'2let done:boolean=false3let count:number=345title =42// Error: a number is not a string
string is text, boolean is true or false, number is a number.
You need very little to start. This course uses TypeScript lightly and explains each new piece when it appears. If you know JavaScript but not TypeScript, read : string as a comment the editor checks for you.
React16 / 204
Module 2 · Setting up your first project
Save a file and the page updates
While npm run dev runs, Vite watches your files. Save a change and the page in the browser updates within a moment, usually without losing what is on screen. This is hot module replacement, or HMR.
Replace the starter App.tsx with this, save, and watch the browser.
QuizInteractive · disabled in the PDF
You close the terminal window that runs npm run dev. What happens to http://localhost:5173?
You close the terminal window that runs npm run dev. What happens to http://localhost:5173?
React17 / 204
Module 2 · Setting up your first project
Errors are messages, so read them
Errors show up in three places: underlined in your editor, as an overlay on the page from Vite, and in the browser’s console. Open the console with F12 (or Cmd+Option+I on a Mac) and choose the Console tab. An error says what went wrong and where: a file name and a line number.
Line 3, column 3. Start at the line it points to, then look just above it.
Find the bugInteractive · disabled in the PDF
This file won’t compile. Which line is wrong?
TSX
Install React Developer Tools. It is a free browser extension from the React team. It adds a Components tab to the browser’s developer tools, where you can see your components and their data. You will use it throughout the course.
React18 / 204
Module 2 · Setting up your first project
Recap
Node.js runs JavaScript tools on your computer; npm downloads packages.
npm create vite@latest makes a project; npm install fetches packages; npm run dev serves it.
React apps have one HTML page with an empty div; main.tsx hands it to React.
TypeScript adds types your editor checks. The browser never sees them.
Errors give a file and a line. Read them first, in the editor, the overlay or the console.
FlashcardsInteractive · disabled in the PDF
Terminal
A window for typing commands.
npm
Node’s package manager: installs libraries and runs scripts.
Vite
A build tool: a fast dev server while you work, small files when you publish.
Dev server
A program on your computer that serves your app at localhost while you work.
createRoot(el).render(<App />)
Makes React own the element el and draw App in it.
Console
The browser’s panel for messages and errors. Open it with F12.
Module 3: JSX from scratch
React19 / 204
Module 3 · JSX from scratch
JSX lets you write markup inside JavaScript
JSX is a way of writing what looks like HTML straight inside JavaScript code. React uses it to describe what should be on screen. It is not HTML and the browser can’t read it: Vite translates it into plain JavaScript before the browser gets it.
TSX
1const heading =<h1>My to-dos</h1>
A JSX tag, stored in a variable like any other value.
That line would be an error in normal JavaScript. In a .tsx (or .jsx) file it is fine: the file ending tells the tools to expect JSX.
React20 / 204
Module 3 · JSX from scratch
Each tag becomes a plain object called an element
The translation turns each tag into a function call. The call returns a small object, a React element, that records what to draw: which tag, and with which details. Making an element draws nothing. It is a description React reads later.
HTML-like syntax inside JavaScript, translated to function calls before it runs.
React element
A plain object describing one piece of the page. Cheap to make, never changed.
Props
The details an element carries: its attributes and what is inside it (children).
React21 / 204
Module 3 · JSX from scratch
Curly braces open a window back to JavaScript
Inside JSX, { } holds any JavaScript expression: a piece of code that produces a value. A variable, a sum, a function call. React puts the value on the page.
TSX
1const name ='Sam'2const left =334constheader=(5<header>6<h1>{name}’s to-dos</h1>7<p>{left} left, {left *10} minutes of work</p>8<p>Today is {newDate().toDateString()}</p>9</header>10)
Expressions, not statements. An if or a for loop is a statement: it does something but has no value, so it can’t go inside braces. You will see the expression-shaped ways to make choices and loops in a later module.
React22 / 204
Module 3 · JSX from scratch
Try it: braces in a live preview
This code runs: the preview draws what it returns as you type. Ignore the export default function line for now, it’s a component, and you’ll meet those at the end of this module. Change the values, or put your own expression inside a pair of braces.
PlaygroundInteractive · disabled in the PDF
TSX
1const name ='Sam'2const left =334exportdefaultfunctionHeader(){5return(6<header>7<h1>{name}’s to-dos</h1>8<p>{left} left, {left *10} minutes of work</p>9<p>Shouting: {name.toUpperCase()}</p>10</header>11)12}
Try left = 5, or {left * 60} seconds. Remove a brace to see an error.
React23 / 204
Module 3 · JSX from scratch
Check: what ends up on the page
Predict the outputInteractive · disabled in the PDF
What text does this show?
TSX
1const done =22const total =53const p =<p>{done} of {total} done, total - done left</p>
What text does this show?
Fill the blanksInteractive · disabled in the PDF
Show the to-do’s text inside the list item.
TSX
1const text ='Walk the dog'2const item =<li></li>
React24 / 204
Module 3 · JSX from scratch
Attributes are written in camelCase
JSX attributes are passed to JavaScript, so they follow JavaScript’s naming. A few differ from HTML. Values are strings in quotes, or any expression in braces.
HTML
JSX
Why
class="done"
className="done"
class is a reserved word in JavaScript
for="new-todo"
htmlFor="new-todo"
for is reserved too
onclick="…"
onClick={…}
Multi-word names use camelCase: each new word capitalised
{{ }} is not special: the outer braces open JavaScript, the inner ones make an object.
React25 / 204
Module 3 · JSX from scratch
Return one thing: wrap siblings in a fragment
A JSX expression must have one outer tag, because it becomes one value. To group siblings without adding an extra element to the page, use a fragment: <> and </>.
Error
constpage=(<h1>My to-dos</h1><ul>…</ul>)
Fine
constpage=(<><h1>My to-dos</h1><ul>…</ul></>)
The parentheses around multi-line JSX are only there so the code reads well. They don’t change what it means.
React26 / 204
Module 3 · JSX from scratch
Every tag must be closed
HTML forgives a missing </li> or a bare <br>. JSX doesn’t. Tags with nothing inside close themselves with />: <input />, <img />, <br />.
Find the bugInteractive · disabled in the PDF
This JSX won’t compile. Which line is the problem?
TSX
React27 / 204
Module 3 · JSX from scratch
Your first component returns JSX
A component is a function that returns JSX. Its name starts with a capital letter. You use it like a tag, <App />, and React calls the function to find out what to draw.
WalkthroughInteractive · disabled in the PDF
src/App.tsx
1exportdefaultfunctionApp(){2const name ='Sam'3return(4<main>5<h1>{name}’s to-dos</h1>6<ul>7<li>Buy milk</li>8<li>Walk the dog</li>9</ul>10</main>11)12}
Lines 1:export default lets other files import it, as main.tsx does. function App() declares the component.
Lines 2: Ordinary JavaScript can run before the return.
Lines 3-11: The JSX it returns: one main wrapping everything else.
QuizInteractive · disabled in the PDF
You rename it function app() and write <app />. What happens?
You rename it function app() and write <app />. What happens?
React28 / 204
Module 3 · JSX from scratch
Try it: a recipe card
Time to write some JSX that is checked. The preview shows your component as you type; Check runs the tests, which look at the page the way a person would.
Code challengeInteractive · disabled in the PDF
Finish RecipeCard: above the list, add a heading showing title, and a paragraph that reads Serves 4, using servings inside braces.
TSX
1exportdefaultfunctionRecipeCard(){2const title ='Pancakes'3const servings =44return(5<article>6{/* TODO: a heading with the title, then "Serves 4" */}7<ul>8<li>Flour</li>9<li>Eggs</li>10<li>Milk</li>11</ul>12</article>13)14}
React29 / 204
Module 3 · JSX from scratch
Recap
JSX is HTML-like syntax in JavaScript, translated before the browser runs it.
Each tag becomes a React element: a plain object describing what to draw.
{ } holds a JavaScript expression; its value goes on the page.
Attributes are camelCase: className, htmlFor, onClick; style takes an object.
One outer tag per expression (use <>…</>), and close every tag.
A component is a capitalised function that returns JSX.
FlashcardsInteractive · disabled in the PDF
JSX
HTML-like syntax inside JavaScript.
React element
The object a JSX tag becomes: a description, not the DOM itself.
Expression
Code that produces a value, so it can go inside { }.
Fragment
<>…</>: groups siblings without adding an element to the page.
Why className?
class is a reserved word in JavaScript.
Component
A function, named with a capital, that returns JSX.
Module 4: Components
React30 / 204
Module 4 · Components
A page is built from components inside components
Look at any app and you can draw boxes around its parts: a header, a list, each row in the list, a form at the bottom. In React each box can be a component. Small components are easier to read, test and reuse than one giant one.
App
Header
TodoList
TodoItem
TodoItem
AddTodo
The to-do app as a tree of components. A component inside another is its child; the outer one is its parent.
React31 / 204
Module 4 · Components
Define a component once, use it as a tag
src/App.tsx
1functionHeader(){2return<h1>My to-dos</h1>3}45functionTodoList(){6return(7<ul>8<li>Buy milk</li>9<li>Walk the dog</li>10</ul>11)12}1314exportdefaultfunctionApp(){15return(16<main>17<Header/>18<TodoList/>19</main>20)21}
When React draws App, it meets <Header />, calls Header, and puts what it returns in that spot. Then the same for TodoList. This calling of components to find out what to show is called rendering.
Render
React calling your component to get the JSX it should show.
Parent and child
A component that uses another is its parent; the one it uses is its child.
React32 / 204
Module 4 · Components
Try it: add a component
Here is the app from the last slide, running. Write a Footer component that returns a paragraph, then use it inside App as <Footer />, under the list.
PlaygroundInteractive · disabled in the PDF
TSX
1functionHeader(){2return<h1>My to-dos</h1>3}45functionTodoList(){6return(7<ul>8<li>Buy milk</li>9<li>Walk the dog</li>10</ul>11)12}1314exportdefaultfunctionApp(){15return(16<main>17<Header/>18<TodoList/>19</main>20)21}
Try <Header /> twice, or <header /> in lowercase, and see what changes.
React33 / 204
Module 4 · Components
One component per file, shared with import and export
As an app grows, move components into their own files. A file exports what others may use, and other files import it. There are two kinds of export.
A file can have many named exports but only one default. Named exports keep the same name everywhere, which makes code easier to search, so many teams prefer them. ./Header means “the file Header in this folder”; the .tsx ending is left off.
Fill the blanksInteractive · disabled in the PDF
TodoList.tsx has export function TodoList(). Import it in App.tsx.
TSX
1importfrom''
React34 / 204
Module 4 · Components
The same component can appear many times
Each <Tag /> is a separate use of the component. Write it three times and React calls the function three times, drawing three copies. Right now every copy is identical; the next module shows how to give each one its own data.
When React renders Page above, how many times does it call Divider?
When React renders Page above, how many times does it call Divider?
React35 / 204
Module 4 · Components
Try it: five stars
A book review shows a rating as stars. Write the star once, as a component, and use it as many times as you need.
Code challengeInteractive · disabled in the PDF
Finish Star so it returns <span>★</span>, and make Rating show five <Star />s inside its paragraph.
TSX
1functionStar(){2// TODO: return a span holding ★3returnnull4}56exportdefaultfunctionRating(){7return(8<section>9<h2>The Hobbit</h2>10<p>11<Star/>12</p>13</section>14)15}
React36 / 204
Module 4 · Components
Define components at the top level of a file
You can write a function inside another function, but never define a component inside another component. Each render would make a brand new function, React would see a different component every time, and it would throw away and rebuild that part of the page, losing anything typed into it.
A pure function always returns the same result for the same inputs and changes nothing outside itself. React expects components to be pure: rendering only works out what to show. It must not change variables outside the component, talk to a server, or touch the DOM.
Find the bugInteractive · disabled in the PDF
This component gives a different result each time React calls it. Which line breaks purity?
TSX
Where do side effects go, then?. Changing things is what apps do, so it has to happen somewhere. It goes in event handlers (code that runs on a click or key press) and, rarely, in effects. Both come later. Rendering itself stays pure.
React38 / 204
Module 4 · Components
Strict Mode renders twice to catch impure code
Remember <StrictMode> in main.tsx? While you develop, it calls each component twice and keeps one result. A pure component gives the same answer both times, so you notice nothing. An impure one shows its bug straight away. In the published app, it renders once.
QuizInteractive · disabled in the PDF
In development, a console.log inside a component prints twice per render. Is something broken?
In development, a console.log inside a component prints twice per render. Is something broken?
React39 / 204
Module 4 · Components
Check: from a design to components
Put in orderInteractive · disabled in the PDF
You are given a picture of the to-do app. Put the steps of turning it into components in order.
1.Draw a box around each part of the picture and name it
2.Use the children inside their parents, starting from App
3.Write each box as a component that returns static JSX
4.Arrange the boxes into a tree: which box sits inside which
React40 / 204
Module 4 · Components
Recap
Split a page into components: boxes inside boxes, forming a tree.
Rendering is React calling your components to find out what to show.
Put components in their own files; share them with export and import.
Define components at the top level of a file, never inside another component.
Components must be pure: same inputs, same JSX, nothing changed outside.
Strict Mode renders twice in development to catch impurities.
FlashcardsInteractive · disabled in the PDF
Rendering
React calling a component to get its JSX.
Named vs default export
Named: import { A }, many per file. Default: import A, one per file.
Pure component
Same inputs give the same JSX, and nothing outside is changed.
Why not define a component inside another?
It is a new function each render, so React rebuilds that part of the page and loses its data.
Strict Mode
Development-only checks; renders components twice to expose impure code.
Module 5: Props: passing data down
React41 / 204
Module 5 · Props: passing data down
Props are a component’s inputs
So far every TodoItem would show the same thing. Props (short for properties) let a parent pass information to each child, the way attributes pass information to an HTML tag. The parent writes them as attributes; React hands them to the child as one object.
TSX
1functionTodoItem(props:{ text:string}){2return<li>{props.text}</li>3}45functionTodoList(){6return(7<ul>8<TodoItemtext="Buy milk"/>9<TodoItemtext="Walk the dog"/>10</ul>11)12}
Same component, different props, different rows.
props: { text: string } is TypeScript saying “props is an object with a text that is a string”. If a parent forgets text, or passes a number, your editor underlines it.
React42 / 204
Module 5 · Props: passing data down
Unpack props right in the function’s brackets
Writing props. everywhere gets long. JavaScript can destructure an object: pull its fields out into variables of the same name. Most React code does it in the parameter list, and names the props’ type so it reads well.
One input to a component, written like an attribute by its parent.
Destructuring
{ text, done } = props: taking fields out of an object into variables.
`type`
A TypeScript name for a shape of data, used to check it.
Note.done ? 'done' : '' is the ternary operator: “if done, then 'done', else an empty string”. It is an expression, so it fits in braces. You will use it a lot.
React43 / 204
Module 5 · Props: passing data down
Check: write a component with props
Fill the blanksInteractive · disabled in the PDF
Complete the component so <Greeting name="Sam" /> shows “Hello, Sam”.
What does React pass as the first argument to TodoItem for <TodoItem text="Buy milk" done={false} />?
What does React pass as the first argument to TodoItem for <TodoItem text="Buy milk" done={false} />?
React44 / 204
Module 5 · Props: passing data down
A prop can be any JavaScript value
Text goes in quotes. Anything else goes in braces: numbers, true or false, arrays, objects, even functions. A prop written with no value at all means true.
highlighted alone is the same as highlighted={true}.
Numbers need braces.priority="2" passes the text "2", not the number 2. TypeScript will catch it if the prop’s type is number.
React45 / 204
Module 5 · Props: passing data down
Try it: one component, different props
The same BookCard is used three times, each with its own props. Change a prop, add a fourth book, or give BookCard a new prop, such as year, and show it.
Try pages="412" in quotes: TypeScript points out that it’s text, not a number.
React46 / 204
Module 5 · Props: passing data down
Give optional props a default value
A ? after a prop’s name in the type makes it optional. When the parent leaves it out, its value is undefined. Give it a default with = in the destructuring.
TSX
1typeBadgeProps={2 label:string3 tone?:'info'|'warning'4}56functionBadge({ label, tone ='info'}:BadgeProps){7return<spanclassName={'badge '+ tone}>{label}</span>8}910<Badgelabel="Today"/>// tone is 'info'11<Badgelabel="Late"tone="warning"/>
'info' | 'warning' is a union type: the value must be one of those exact strings. Pass tone="red" and the editor objects.
React47 / 204
Module 5 · Props: passing data down
Try it: a greeting with a default
Code challengeInteractive · disabled in the PDF
Make Greeting show Hello, Ada! for <Greeting name="Ada" />, and Hello, friend! when no name is given. Use a default value in the destructuring.
TSX
1typeGreetingProps={2 name?:string3}45exportdefaultfunctionGreeting({ name }:GreetingProps){6// TODO: greet name, or 'friend' when it's missing7return<p>Hello!</p>8}
React48 / 204
Module 5 · Props: passing data down
children: what goes between the tags
Whatever you put between a component’s opening and closing tags arrives as a special prop called children. It lets you build wrappers, like a card or a panel, that don’t need to know what they hold.
TSX
1importtype{ ReactNode }from'react'23functionCard({ title, children }:{4 title:string5 children:ReactNode6}){7return(8<sectionclassName="card">9<h2>{title}</h2>10{children}11</section>12)13}1415<Cardtitle="Today">16<TodoList/>17</Card>
ReactNode is the type for anything React can draw: JSX, text, numbers, null, or lists of them.
React49 / 204
Module 5 · Props: passing data down
Props are read-only
A component must never change its props. They belong to the parent, and React relies on them staying put while it renders. When something needs to change, the parent passes new props, and React renders the child again with them.
Find the bugInteractive · disabled in the PDF
This component breaks a rule of props. Which line?
TSX
React50 / 204
Module 5 · Props: passing data down
Data flows one way: down
Props go from parent to child, never back up. This is one-way data flow. If you want to know where a value came from, follow the props upward until you find the component that made it. That makes React apps easier to debug.
QuizInteractive · disabled in the PDF
A TodoItem needs to tell the app it was ticked. With what you know about props, how could it?
A TodoItem needs to tell the app it was ticked. With what you know about props, how could it?
React51 / 204
Module 5 · Props: passing data down
Recap
Props are a component’s inputs, written as attributes by its parent.
React passes them as one object; destructure it in the parameters.
Strings in quotes; anything else in braces. A bare prop means true.
Optional props get ? in the type and a default with =.
children holds what goes between the tags.
Props are read-only and flow one way, from parent to child.
FlashcardsInteractive · disabled in the PDF
Props
The inputs a parent passes to a child component.
children
The prop holding whatever is between a component’s tags.
ReactNode
The TypeScript type for anything React can draw.
One-way data flow
Data moves from parents to children through props, never back up.
Ternary operator
a ? b : c: if a then b, else c. An expression.
Union type
'a' | 'b': the value must be one of these.
Module 6: Lists and conditions
React52 / 204
Module 6 · Lists and conditions
Keep the data in an array, not in the markup
Writing each <TodoItem /> by hand only works for a fixed list. Real lists come from data: an array (an ordered list of values, in [ ]) of objects (groups of named values, in { }). Describe each to-do once, as data, and let code make the rows.
src/todos.ts
1exporttypeTodo={2 id:number3 text:string4 done:boolean5}67exportconst starterTodos:Todo[]=[8{id:1,text:'Buy milk',done:false},9{id:2,text:'Walk the dog',done:true},10{id:3,text:'Call the dentist',done:false}11]
Todo[] means “an array of Todo”. This shape is the running example from here on.
React53 / 204
Module 6 · Lists and conditions
map turns each item into an element
Arrays have a method called map. It runs a function on every item and returns a new array of the results. Map to-dos to JSX and you get an array of elements, which React draws in order.
WalkthroughInteractive · disabled in the PDF
src/TodoList.tsx
1import{type Todo }from'./todos'2import{ TodoItem }from'./TodoItem'34exportfunctionTodoList({ todos }:{ todos:Todo[]}){5return(6<ul>7{todos.map((todo)=>(8<TodoItem9key={todo.id}10text={todo.text}11done={todo.done}12/>13))}14</ul>15)16}
Lines 4: The list gets the array as a prop.
Lines 7:todos.map((todo) => …) calls the arrow function once per to-do, with that to-do as todo.
Lines 8-12: Each call returns one TodoItem element with that to-do’s data.
Lines 9:key is a special prop that tells React which row is which. Next slide.
React54 / 204
Module 6 · Lists and conditions
Keys tell React which row is which
When the list changes, React compares the new rows with the old ones. A key is a name for each row, unique among its siblings. With keys, React knows that “Buy milk” moved, rather than thinking every row changed. Without them it warns you in the console.
The position in the array is a poor key: delete the first to-do and every other row’s index shifts by one, so React matches rows to the wrong data. Use a stable id that belongs to the item itself.
QuizInteractive · disabled in the PDF
Which is the best key for a to-do row?
Which is the best key for a to-do row?
React55 / 204
Module 6 · Lists and conditions
Filter first, then map
filter is another array method: it keeps only the items for which your function returns true. Chain it before map to show part of a list.
TSX
1const open = todos.filter((todo)=>!todo.done)23return(4<ul>5{open.map((todo)=>(6<TodoItemkey={todo.id}text={todo.text}done={false}/>7))}8</ul>9)
! means “not”: keep the to-dos that are not done.
Fill the blanksInteractive · disabled in the PDF
Show only the finished to-dos, each with its id as key.
The list below is drawn from an array. Add a to-do to the array, flip a done, or change the filter to keep the finished ones, and watch the preview follow the data.
PlaygroundInteractive · disabled in the PDF
TSX
1const todos =[2{id:1,text:'Buy milk',done:false},3{id:2,text:'Walk the dog',done:true},4{id:3,text:'Water the plants',done:false}5]67exportdefaultfunctionTodoList(){8const open = todos.filter((todo)=>!todo.done)9return(10<ul>11{open.map((todo)=>(12<likey={todo.id}>{todo.text}</li>13))}14</ul>15)16}
Try todos.map without the filter, and show the done ones with <s>.
React57 / 204
Module 6 · Lists and conditions
Use a normal if to return something else
A component is a function, so you can use an ifbefore the JSX. Returning early is the clearest way to handle a whole different screen, like an empty list.
TSX
1functionTodoList({ todos }:{ todos:Todo[]}){2if(todos.length ===0){3return<pclassName="empty">Nothing to do. Enjoy.</p>4}5return(6<ul>7{todos.map((todo)=>(8<TodoItemkey={todo.id}text={todo.text}done={todo.done}/>9))}10</ul>11)12}
=== checks that two values are equal. .length is how many items an array has.
React58 / 204
Module 6 · Lists and conditions
Inside JSX: the ternary and &&
Inside braces you need expressions. Two cover nearly everything: a ? b : c to choose between two things, and a && b to show b only when a is true. React draws nothing for null, undefined, true and false.
1functionDoneCount({ todos }:{ todos:Todo[]}){2const done = todos.filter((todo)=> todo.done).length3if(done ===0)returnnull4return<p>{done} done</p>5}
Find the bugInteractive · disabled in the PDF
The list renders nothing at all. Which line is the bug?
TSX
React60 / 204
Module 6 · Lists and conditions
Try it: a list with an empty state
Put the module together: filter, map with keys, and a different screen when there is nothing to show.
Code challengeInteractive · disabled in the PDF
Make TodoList show only the to-dos that are not done, one li each. When none are left, show a paragraph Nothing to do. Enjoy. instead of the list.
TSX
1typeTodo={ id:number; text:string; done:boolean}23const sample:Todo[]=[4{id:1,text:'Buy milk',done:false},5{id:2,text:'Walk the dog',done:true}6]78exportdefaultfunctionTodoList({ todos = sample }){9// TODO: keep the open ones, and handle an empty list10return(11<ul>12{todos.map((todo)=>(13<likey={todo.id}>{todo.text}</li>14))}15</ul>16)17}
React61 / 204
Module 6 · Lists and conditions
Recap
Keep lists as arrays of objects, and turn them into rows with map.
Every row needs a key: unique among its siblings and stable, like an id.
Use filter before map to show part of a list.
Use if and an early return for a different screen; null shows nothing.
Inside JSX, use a ? b : c and a && b, and never let a 0 sneak through.
FlashcardsInteractive · disabled in the PDF
array.map(fn)
A new array with fn applied to each item.
array.filter(fn)
A new array of the items for which fn returns true.
Key
A stable, unique name for a row among its siblings, so React can match rows.
Why not the index as key?
It shifts when items are added, removed or moved.
What does React draw for false and null?
Nothing. But 0 is drawn.
Module 7: Events and state
React62 / 204
Module 7 · Events and state
Pass a function to onClick
To react to a click, give an element an onClick prop holding a function. This function is an event handler. React calls it when the event happens, not before. Handlers are where side effects belong: they may change things, unlike rendering.
By habit, handlers are named handle plus the event: handleClick, handleSubmit.
Find the bugInteractive · disabled in the PDF
The alert pops up as soon as the page loads, not on click. Which line?
TSX
React63 / 204
Module 7 · Events and state
Parents pass handlers down as props
A row doesn’t own the list, so it can’t change it. Instead the parent passes a function, and the row calls it with what happened. By habit, such props are named on plus the event: onToggle, onDelete.
TSX
1typeProps={2 todo:Todo3onToggle:(id:number)=>void4}56functionTodoItem({ todo, onToggle }:Props){7return(8<li>9<input10type="checkbox"11checked={todo.done}12onChange={()=>onToggle(todo.id)}13/>14{todo.text}15</li>16)17}
(id: number) => void is the type of a function that takes a number and returns nothing.
React64 / 204
Module 7 · Events and state
Handlers get an event object
React calls a handler with an event object describing what happened. Two of its methods matter most. preventDefault() stops the browser’s own reaction, such as a form reloading the page. stopPropagation() stops the event from also reaching the elements around this one.
You click the button five times. What does it say?
You click the button five times. What does it say?
React66 / 204
Module 7 · Events and state
useState gives a component memory
State is data a component remembers between renders. useState is a Hook: a special React function, named with use, that hooks a component into a React feature. It returns two things: the current value, and a function to change it.
Lines 4:useState(0) makes a piece of state starting at 0. The [a, b] on the left is array destructuring: name the two things it returns.
Lines 6: Calling setCount stores the new value and asks React to render Counter again.
Lines 4: On the next render useState(0) returns the stored value, not 0. The 0 only counts the first time.
State
Data a component remembers between renders; changing it renders again.
Hook
A React function whose name starts with use, like useState.
Setter
The function useState returns for changing the value, like setCount.
React67 / 204
Module 7 · Events and state
Try it: a counter
The state is there, but nothing changes it yet. Give each button an onClick that calls the setter.
Code challengeInteractive · disabled in the PDF
Make the first button count its clicks (Clicked 1 times, Clicked 2 times…), and make Reset set the count back to 0.
TSX
1import{ useState }from'react'23exportdefaultfunctionCounter(){4const[count, setCount]=useState(0)5// TODO: an onClick on each button6return(7<div>8<button>Clicked {count} times</button>9<button>Reset</button>10</div>11)12}
React68 / 204
Module 7 · Events and state
Call Hooks at the top level, every render
React tells a component’s pieces of state apart by the order the Hooks are called in. So Hooks must be called in the same order on every render: at the top level of a component, never inside an if, a loop or a nested function.
Don’t
functionTodoItem({ todo }:Props){if(todo.done){const[open, setOpen]=useState(false)}// …}
Do
functionTodoItem({ todo }:Props){const[open, setOpen]=useState(false)if(todo.done){// use open here}// …}
Tip. The Vite template includes a linter, a tool that reads your code for mistakes, with the React Hooks rules turned on. Run npm run lint, and your editor can show its warnings as you type.
React69 / 204
Module 7 · Events and state
Trigger, render, commit
Every screen update goes through three steps. A render is triggered (by the first load, or by a setter). React renders: it calls your components to get new JSX and compares it with the last result. Then it commits: it changes only the DOM that differs.
Put in orderInteractive · disabled in the PDF
You click the counter. Put what happens in order.
1.React calls Counter; useState now returns 1
2.React stores 1 and schedules a render of Counter
3.React updates that text in the DOM, and the browser draws it
4.React compares the new JSX with the old and finds the text changed
5.The onClick handler calls setCount(1)
React70 / 204
Module 7 · Events and state
Each copy of a component has its own state
State belongs to one place in the tree. Render two <Counter />s and each keeps its own count. State is also private: a parent can’t read a child’s state. If two parts need the same data, it has to live higher up (a later module).
QuizInteractive · disabled in the PDF
A page shows <Counter /> twice. You click the first one three times. What do they show?
A page shows <Counter /> twice. You click the first one three times. What do they show?
React71 / 204
Module 7 · Events and state
Try it: two counters, two memories
Click each counter a few times: each keeps its own count. Then try the ideas below and watch what changes.
Local variables don’t survive renders or trigger them. State does both.
const [value, setValue] = useState(initial); the setter stores and schedules a render.
Hooks: top level only, same order every render.
An update is trigger, render, commit. State is private to each instance.
FlashcardsInteractive · disabled in the PDF
Event handler
A function React calls when an event happens, like a click.
Bubbling
An event reaching the element, then each of its parents in turn.
useState(initial)
Returns [value, setValue]. initial only counts on the first render.
Rules of Hooks
Call them at the top level of a component or custom Hook, never conditionally.
Commit
The step where React applies the differences to the DOM.
Module 8: Updating state the right way
React73 / 204
Module 8 · Updating state the right way
State is a snapshot for one render
Inside one render, a state variable never changes. Calling the setter doesn’t change count in the code that is running: it asks for the next render to have a new value. Think of each render as a photo, with its own fixed state.
Predict the outputInteractive · disabled in the PDF
count is 0. You click once. What does it show after?
count is 0. You click once. What does it show after?
React74 / 204
Module 8 · Updating state the right way
React batches updates and renders once
When a handler calls several setters, React doesn’t render after each one. It collects them in a queue and renders once when the handler has finished. This is batching. It is why setting three pieces of state in a row is fast and never shows a half-updated screen.
TSX
1functionhandleAdd(){2setTodos([...todos, newTodo])3setDraft('')4setMessage('Added')5// One render, with all three changes.6}
Snapshot
The fixed values of state during one render.
Batching
React grouping the setter calls of one event into a single render.
React75 / 204
Module 8 · Updating state the right way
To build on the latest value, pass a function
A setter also accepts a function, an updater. React calls it with the most recent value in the queue, so updates stack correctly. Use one whenever the new value depends on the old.
setCount((c)=> c +1)// 0 → 1setCount((c)=> c +1)// 1 → 2// next render: 2
Fill the blanksInteractive · disabled in the PDF
Toggle open from whatever it currently is, safely.
TSX
1setOpen(()=>)
React76 / 204
Module 8 · Updating state the right way
Try it: snapshot or updater
Both buttons call a setter three times. Click each and compare. Then change the first to use an updater, or mix the two kinds, and predict the result before you click.
PlaygroundInteractive · disabled in the PDF
TSX
1import{ useState }from'react'23exportdefaultfunctionScore(){4const[score, setScore]=useState(0)56functionaddThreeWithValues(){7setScore(score +1)8setScore(score +1)9setScore(score +1)10}1112functionaddThreeWithUpdaters(){13setScore((s)=> s +1)14setScore((s)=> s +1)15setScore((s)=> s +1)16}1718return(19<div>20<p>Score: {score}</p>21<buttononClick={addThreeWithValues}>+3 with values</button>22<buttononClick={addThreeWithUpdaters}>+3 with updaters</button>23</div>24)25}
Try setScore(score + 5) followed by setScore((s) => s + 1): what does one click add?
React77 / 204
Module 8 · Updating state the right way
Replace objects, don’t change them
React decides whether state changed by asking: is it the same object as before? If you change a field inside the old object, it is still the same object, so React may skip the render. Treat state as immutable (never changed): make a new object instead. The spread syntax ... copies all the fields of an object into a new one.
Mutation: may not render
settings.showDone =falsesetSettings(settings)
New object: renders
setSettings({...settings,showDone:false})
Mutate
Change an object or array in place.
Immutable
Never changed after it is made; you make a new one instead.
Spread `...`
Copies the fields of an object (or items of an array) into a new one.
React78 / 204
Module 8 · Updating state the right way
Add and remove items with new arrays
The same rule holds for arrays. Methods like push, splice and sort change the array in place, so avoid them on state. Use ones that return a new array.
To
Avoid (mutates)
Use (new array)
Add
push, unshift
[...todos, todo], [todo, ...todos]
Remove
splice
todos.filter((t) => t.id !== id)
Change
todos[i] = …
todos.map(…)
Sort
sort, reverse
toSorted, toReversed, or copy first
TSX
1functionaddTodo(text:string){2const todo ={id: Date.now(), text,done:false}3setTodos((todos)=>[...todos, todo])4}56functiondeleteTodo(id:number){7setTodos((todos)=> todos.filter((t)=> t.id !== id))8}
Date.now() (milliseconds since 1970) is a quick id for learning. Real apps often use crypto.randomUUID().
React79 / 204
Module 8 · Updating state the right way
Change one item with map
To tick a to-do, make a new array where that one item is a new object and every other item is left as it is.
TSX
1functiontoggleTodo(id:number){2setTodos((todos)=>3 todos.map((t)=>(t.id === id ?{...t,done:!t.done }: t))4)5}
Find the bugInteractive · disabled in the PDF
Ticking a box sometimes doesn’t update the screen. Which line is the bug?
TSX
React80 / 204
Module 8 · Updating state the right way
Try it: tick and delete
Code challengeInteractive · disabled in the PDF
Write toggle and remove with new arrays: ticking a box marks that to-do done (so 1 left), and its Delete button takes it off the list.
TSX
1import{ useState }from'react'23const start =[4{id:1,text:'Buy milk',done:false},5{id:2,text:'Walk the dog',done:false}6]78exportdefaultfunctionTodos(){9const[todos, setTodos]=useState(start)10functiontoggle(id:number){11// TODO: map to a new array, with a new object for this one12}13functionremove(id:number){14// TODO: a new array without this one15}16const rows = todos.map((t)=>(17<likey={t.id}>18<label>19<inputtype="checkbox"checked={t.done}20onChange={()=>toggle(t.id)}/>21{t.text}22</label>23<buttononClick={()=>remove(t.id)}>Delete</button>24</li>25))26const left = todos.filter((t)=>!t.done).length27return<div><p>{left} left</p><ul>{rows}</ul></div>28}
React81 / 204
Module 8 · Updating state the right way
Nested data: copy every level you change
Spread only copies one level. If state holds objects inside objects, copy each level on the way down to the field you change. When this gets painful, the data is probably nested more deeply than it needs to be: keep state flat, with lists of items that point to each other by id.
A new list and a new owner. Everything else is shared with the old one, which is fine because nothing is ever mutated.
React82 / 204
Module 8 · Updating state the right way
Don’t store what you can work out
Keep state minimal: if a value can be computed from props or other state, compute it during render instead of storing it. Duplicated state drifts out of step, and then you have the DOM-by-hand problem again.
Two copies of the truth
const[todos, setTodos]=useState(start)const[left, setLeft]=useState(2)// every change must update both
One truth, one calculation
const[todos, setTodos]=useState(start)const left = todos.filter((t)=>!t.done).length
QuizInteractive · disabled in the PDF
In a to-do app with a filter, which of these should be state? Pick all.
In a to-do app with a filter, which of these should be state? Pick all.
React83 / 204
Module 8 · Updating state the right way
Recap
State is a snapshot: setters change the next render, not the variable you hold.
React batches the setters of one event into one render.
Use an updater, setX((x) => …), when the next value depends on the last.
Never mutate state. Make new objects and arrays with spread, map and filter.
Keep state flat and minimal; compute everything else during render.
FlashcardsInteractive · disabled in the PDF
Updater function
setX((prev) => next): built on the latest queued value.
How does React spot a change?
It compares old and new with Object.is: same object means no change.
Add to an array in state
[...items, item]
Remove from an array in state
items.filter((i) => i.id !== id)
Change one item
items.map((i) => i.id === id ? { ...i, … } : i)
Minimal state
Store only what can’t be computed from other props or state.
Module 9: Forms and inputs
React84 / 204
Module 9 · Forms and inputs
A controlled input shows state and reports changes
An input keeps its own text, like a tiny piece of state inside the browser. To let React own it instead, pass value from state and update that state in onChange. This is a controlled input: what it shows is always your state.
Lines 6: On every key press React calls onChange. event.target is the input; .value is its new text. Store it, React renders, and the input shows it.
Note. In React, onChange fires on every key press, like the browser’s input event, not only when the field loses focus.
React85 / 204
Module 9 · Forms and inputs
Try it: state follows every key
Type in the box. Because draft is state, everything else on the screen can use it as you type. Then take out the onChange and try typing again.
PlaygroundInteractive · disabled in the PDF
TSX
1import{ useState }from'react'23exportdefaultfunctionDraft(){4const[draft, setDraft]=useState('')5return(6<div>7<label>8 What needs doing?{' '}9<input10value={draft}11onChange={(event)=>setDraft(event.target.value)}12/>13</label>14<p>{draft.length} of 40 characters</p>15<p>Preview: {draft.toUpperCase()}</p>16</div>17)18}
Try setDraft(event.target.value.slice(0, 40)) to stop at 40, or useState('Buy milk') to start filled in.
React86 / 204
Module 9 · Forms and inputs
Check: a frozen input
Find the bugInteractive · disabled in the PDF
You can’t type in this input: every key press is ignored. Which line is missing a piece?
TSX
React87 / 204
Module 9 · Forms and inputs
Handle the form’s submit, not the button’s click
Wrap inputs in a <form> and handle onSubmit. It fires on a button click and on pressing Enter. By default the browser reloads the page on submit, so call preventDefault() first.
src/AddTodo.tsx
1typeProps={onAdd:(text:string)=>void}23exportfunctionAddTodo({ onAdd }:Props){4const[draft, setDraft]=useState('')56functionhandleSubmit(event:React.SubmitEvent){7 event.preventDefault()8const text = draft.trim()9if(text ==='')return10onAdd(text)11setDraft('')12}1314return(15<formonSubmit={handleSubmit}>16<inputvalue={draft}onChange={(e)=>setDraft(e.target.value)}/>17<buttondisabled={draft.trim()===''}>Add</button>18</form>19)20}
trim() removes spaces at both ends. The button is disabled while there is nothing to add.
React88 / 204
Module 9 · Forms and inputs
Try it: a form that adds to-dos
Code challengeInteractive · disabled in the PDF
Finish handleSubmit: add the trimmed draft to todos (as { id, text }), then empty the field. Ignore a draft that is only spaces.
TSX
1import{ useState,type SubmitEvent }from'react'23typeTodo={ id:number; text:string}45exportdefaultfunctionTodoForm(){6const[todos, setTodos]=useState<Todo[]>([])7const[draft, setDraft]=useState('')89functionhandleSubmit(event:SubmitEvent){10 event.preventDefault()11// TODO: add the trimmed draft, unless it's empty; then clear it12}1314return(15<formonSubmit={handleSubmit}>16<label>17 New to-do{' '}18<input19value={draft}20onChange={(e)=>setDraft(e.target.value)}21/>22</label>23<button>Add</button>24<ul>25{todos.map((t)=><likey={t.id}>{t.text}</li>)}26</ul>27</form>28)29}
React89 / 204
Module 9 · Forms and inputs
Checkboxes, selects and text areas
Every form control is controlled the same way, with one difference: a checkbox uses checked, not value, and you read event.target.checked.
TSX
1<input2type="checkbox"3checked={showDone}4onChange={(e)=>setShowDone(e.target.checked)}5/>67<selectvalue={sort}onChange={(e)=>setSort(e.target.value)}>8<optionvalue="newest">Newest first</option>9<optionvalue="az">A to Z</option>10</select>1112<textareavalue={notes}onChange={(e)=>setNotes(e.target.value)}/>
In React, select and textarea take value too, unlike in plain HTML.
A <label> names an input. Screen readers (software that reads the page aloud for people who can’t see it) announce it, and clicking it focuses the input. Link them with htmlFor and id. Ids must be unique on the page, so when a component can appear more than once, let React make one with the useId Hook.
TSX
1import{ useId }from'react'23functionTextField({ label,...rest }:FieldProps){4const id =useId()5return(6<>7<labelhtmlFor={id}>{label}</label>8<inputid={id}{...rest}/>9</>10)11}
...rest collects the other props; {...rest} spreads them onto the input.
A placeholder is not a label. Placeholder text disappears as soon as someone types, is often too faint to read, and isn’t reliably announced. Use it for an example, never as the only name.
React91 / 204
Module 9 · Forms and inputs
Uncontrolled inputs and FormData
You don’t have to control every input. An uncontrolled input keeps its own value; give it a name and an optional defaultValue, and read everything when the form is sent with FormData, a browser object holding each named field.
TSX
1functionhandleSubmit(event:React.SubmitEvent<HTMLFormElement>){2 event.preventDefault()3const data =newFormData(event.currentTarget)4onAdd(String(data.get('text')))5 event.currentTarget.reset()6}78<formonSubmit={handleSubmit}>9<inputname="text"defaultValue=""/>10<button>Add</button>11</form>
Controlled
Uncontrolled
value + onChange
name + defaultValue
React has the value on every key press
You read it when you need it
For live checks, disabling buttons, formatting
For simple forms; fits React 19 form actions
React92 / 204
Module 9 · Forms and inputs
Editing a to-do in place
Double-click a to-do to edit it. The row needs its own state: whether it is editing, and the draft text. The list still owns the to-dos, so saving calls a handler prop.
TSX
1functionTodoItem({ todo, onRename }:Props){2const[editing, setEditing]=useState(false)3const[draft, setDraft]=useState(todo.text)45if(editing){6return(7<li>8<formonSubmit={(e)=>{9 e.preventDefault()10onRename(todo.id, draft)11setEditing(false)12}}>13<inputvalue={draft}14onChange={(e)=>setDraft(e.target.value)}/>15</form>16</li>17)18}19return(20<lionDoubleClick={()=>setEditing(true)}>{todo.text}</li>21)22}
QuizInteractive · disabled in the PDF
Elsewhere the to-do is renamed while this row isn’t editing. Then you double-click it. What does the input show?
Elsewhere the to-do is renamed while this row isn’t editing. Then you double-click it. What does the input show?
React93 / 204
Module 9 · Forms and inputs
Recap
Controlled input: value from state, onChange sets it. Checkboxes use checked.
Handle onSubmit on the form and call preventDefault(). Enter submits too.
Every input needs a real <label>; useId makes unique ids.
Uncontrolled inputs keep their own value; read them with FormData.
useState(initial) ignores later changes to initial.
FlashcardsInteractive · disabled in the PDF
Controlled input
An input whose value comes from React state and changes through onChange.
Uncontrolled input
An input that keeps its own value; you read it when needed.
FormData
A browser object with the value of each named field in a form.
useId()
A Hook that returns an id unique to this component instance.
Screen reader
Software that reads a page aloud, using labels and structure.
Module 10: Sharing state between components
React94 / 204
Module 10 · Sharing state between components
Two components need the same data
The to-do app now has filter buttons (all, open, done) and a list. The buttons decide the filter; the list needs it. If the filter is state inside FilterBar, the list can’t see it: state is private, and props only flow down.
App
FilterBarhas the filter
TodoListneeds the filter
Siblings can’t pass props to each other.
React95 / 204
Module 10 · Sharing state between components
Lift state up to the closest common parent
Lifting state up means moving it to the nearest component above everyone who needs it. That parent passes the value down as props, and passes a setter-like function to the component that changes it.
useState<Filter> tells TypeScript the state is one of the three filters.
React96 / 204
Module 10 · Sharing state between components
The child becomes controlled by its parent
FilterBar no longer has state. It shows value and reports clicks through onChange, just like a controlled input. A component driven entirely by props is called controlled; one with its own state, uncontrolled.
aria-pressed tells screen readers which button is on. More in the accessibility module.
QuizInteractive · disabled in the PDF
A count of open to-dos now sits in the Header, also a child of App. Where should the to-dos live?
A count of open to-dos now sits in the Header, also a child of App. Where should the to-dos live?
React97 / 204
Module 10 · Sharing state between components
Try it: lift the search up
The search box keeps its query to itself, so the list can’t see it. Typing changes nothing below.
Code challengeInteractive · disabled in the PDF
Move query up into Bookshop. Make SearchBox controlled, with value and onChange props, and pass the query to BookList, so typing in Search narrows the list.
TSX
1import{ useState }from'react'23const books =['Dune','Emma','Beloved','Middlemarch']45functionSearchBox(){6const[query, setQuery]=useState('')7return(8<label>9 Search{' '}10<input11value={query}12onChange={(e)=>setQuery(e.target.value)}13/>14</label>15)16}1718functionBookList({ query }:{ query:string}){19const q = query.toLowerCase()20const shown = books.filter((b)=> b.toLowerCase().includes(q))21return<ul>{shown.map((b)=><likey={b}>{b}</li>)}</ul>22}2324exportdefaultfunctionBookshop(){25// TODO: the query lives here, and goes down to both26return(27<main>28<SearchBox/>29<BookListquery=""/>30</main>31)32}
React98 / 204
Module 10 · Sharing state between components
Deciding where a piece of state lives
Find every component that shows or changes it.
Find their closest common parent.
Put the state there, or in a component in between if that reads better.
Pass the value down as props, and changes up through handler props.
Start low, lift when needed. Keep state as close as possible to where it is used. A to-do row’s “is editing” state belongs in the row. Lift only when a second component needs it. State that is too high makes more of the app render on every change.
React99 / 204
Module 10 · Sharing state between components
Check: lift some state
Put in orderInteractive · disabled in the PDF
The “show finished” checkbox lives in Settings, and now TodoList needs it too. Put the refactor in order.
1.Pass the value to TodoList and Settings as props
2.Move the useState from Settings into that parent
3.Find the closest parent of Settings and TodoList
4.Pass a handler to Settings and call it from the checkbox
React100 / 204
Module 10 · Sharing state between components
State belongs to a position in the tree
React keeps state with a component’s place in the tree, not with the function or the variable. As long as the same component renders in the same spot, its state is kept, even if its props change. Remove it, or render something else there, and its state is thrown away.
QuizInteractive · disabled in the PDF
A to-do row is being edited. You tick “show finished”, so the list re-renders with the same row in the same place. Is the half-typed draft kept?
A to-do row is being edited. You tick “show finished”, so the list re-renders with the same row in the same place. Is the half-typed draft kept?
TSX
1{isAdmin ?<Panel/>:<Panel/>}// same spot: state kept2{isAdmin ?<Panel/>:<Profile/>}// different type: reset
React101 / 204
Module 10 · Sharing state between components
Change the key to start fresh
Keys work everywhere, not only in lists. A different key tells React it is a different component, so it throws the old state away. That is the cleanest way to reset a form when what it edits changes.
Keeps the old draft
<EditTodotodo={selected}/>// switch to another to-do:// the draft still holds the// first one's text
Fresh for each to-do
<EditTodokey={selected.id}todo={selected}/>
Prefer a key to “syncing” state. The tempting fix is an effect that copies the prop into state when it changes. That renders once with the stale value, then again, and is easy to get wrong. A key resets everything in one go, children included.
React102 / 204
Module 10 · Sharing state between components
Recap
When siblings need the same state, lift it to their closest common parent.
Pass values down as props and changes up through handler props.
Keep one source of truth; keep state as low as it can go.
React keeps state by position in the tree and by key.
Change a component’s key to reset its state on purpose.
FlashcardsInteractive · disabled in the PDF
Lifting state up
Moving state to the nearest parent of every component that needs it.
Single source of truth
Each piece of data lives in one place; everything else reads from there.
Controlled component
One driven by its props, reporting changes through callbacks.
When is state kept?
While the same type renders at the same position with the same key.
How to reset a component’s state
Give it a different key.
Module 11: Effects, and when not to use them
React103 / 204
Module 11 · Effects, and when not to use them
Effects keep something outside React in step
Rendering must be pure, and handlers run only on events. But some work has to happen because a component is on screen: setting the browser tab’s title, starting a timer, listening to the window, connecting to a chat server. An effect is code React runs after a commit to keep such an external system in step with your state.
Effect
Code that runs after React updates the screen, to sync with something outside React.
External system
Anything React doesn’t manage: the network, timers, browser APIs, other libraries.
Side effect
Any change outside a function: writing to the network, the DOM, a variable.
React104 / 204
Module 11 · Effects, and when not to use them
useEffect runs after the screen updates
WalkthroughInteractive · disabled in the PDF
TSX
1import{ useEffect }from'react'23functionTodoApp({ todos }:{ todos:Todo[]}){4const left = todos.filter((t)=>!t.done).length56useEffect(()=>{7 document.title = left +' to-dos left'8},[left])910return<TodoListtodos={todos}/>11}
Lines 4: Work out what to show during render, as usual.
Lines 6-8: Give useEffect a function. React runs it after committing, once the screen is up to date.
Lines 8: The dependency array: the values the effect uses. React runs it again only when one of them changed since the last render.
Dependencies
Runs after
[left]
The first render, and each render where left changed
[]
Only the first render (when the component appears)
none at all
Every render. Almost never what you want
React105 / 204
Module 11 · Effects, and when not to use them
Check: when does it run
Predict the outputInteractive · disabled in the PDF
The component renders with filter going all, all, done. How many times does the effect run?
The component renders with filter going all, all, done. How many times does the effect run?
Don’t lie about dependencies. Every value from the component that the effect reads belongs in the array. Leave one out and the effect keeps using an old snapshot. The linter’s exhaustive-deps rule checks this for you; trust it.
React106 / 204
Module 11 · Effects, and when not to use them
Try it: keep the tab’s title in step
The browser tab’s title lives outside React, in document.title. Rendering can’t set it (that would be a side effect), but an effect can, after each render where the count changed.
Code challengeInteractive · disabled in the PDF
Add an effect that sets document.title to Inbox (3) while 3 are unread, and to just Inbox when none are. It should run again whenever unread changes.
TSX
1import{ useEffect, useState }from'react'23exportdefaultfunctionInbox(){4const[unread, setUnread]=useState(3)5// TODO: an effect that keeps document.title in step6return(7<div>8<p>{unread} unread</p>9<buttononClick={()=>setUnread(unread -1)}>Read one</button>10</div>11)12}
React107 / 204
Module 11 · Effects, and when not to use them
Return a cleanup to undo what the effect did
If an effect starts something, it must stop it. Return a function from the effect: React calls this cleanup before running the effect again and when the component unmounts (leaves the screen). Appearing on screen is called mounting.
setInterval calls a function every 1000 ms; clearInterval stops it. useState(() => …) computes the start value once.
Why effects run twice in development. Strict Mode mounts each component, cleans its effects up, and sets them up again, once, in development. If that causes a bug (two timers, two connections), your cleanup is missing or incomplete.
React108 / 204
Module 11 · Effects, and when not to use them
Try it: see a cleanup at work
The timer’s effect starts an interval that counts ticks in its parent, and its cleanup stops it. Hide the timer and the ticks stop. Then break it, as the caption says.
PlaygroundInteractive · disabled in the PDF
TSX
1import{ useEffect, useState }from'react'23functionTimer({ onTick }:{onTick:()=>void}){4useEffect(()=>{5const timer =setInterval(onTick,1000)6return()=>clearInterval(timer)7},[])8return<p>A timer is running.</p>9}1011exportdefaultfunctionApp(){12const[shown, setShown]=useState(true)13const[ticks, setTicks]=useState(0)14return(15<div>16<p>Ticks: {ticks}</p>17<buttononClick={()=>setShown(!shown)}>18{shown ?'Hide':'Show'} the timer19</button>20{shown &&<TimeronTick={()=>setTicks((t)=> t +1)}/>}21</div>22)23}
Delete the return line, then hide and show the timer a few times: the old intervals never stop, and the ticks speed up.
React109 / 204
Module 11 · Effects, and when not to use them
Check: a leaking listener
Find the bugInteractive · disabled in the PDF
After opening and closing this panel a few times, every key press runs the handler many times. Which line is the bug?
TSX
React110 / 204
Module 11 · Effects, and when not to use them
You don’t need an effect to compute a value
Effects are for outside systems. A common mistake is using one to keep state in step with other state. That renders once with the stale value, then again with the right one. Compute it during render instead.
If code runs because the user did something, put it in that event’s handler. Effects run because the component rendered, and then you lose track of why. Ask: is this caused by being on screen, or by an action?
Read the latest value without re-running: useEffectEvent
Sometimes an effect needs a value that shouldn’t restart it. A chat connection depends on the room, but the “connected” message also uses the theme, and changing the theme shouldn’t reconnect. React 19.2 added useEffectEvent for this: a function the effect calls that always sees the latest props and state, and isn’t a dependency.
Only roomId restarts the effect. onConnected always sees the latest theme.
Expert note. Only call an effect event from inside effects: never during render, and don’t pass it to other components. Before 19.2, people used a ref holding the latest value for the same job; you will see that in older code.
React113 / 204
Module 11 · Effects, and when not to use them
Recap
Effects sync with outside systems, after the commit.
List every value the effect reads as a dependency; trust the linter.
Return a cleanup that undoes the setup. Strict Mode tests it in development.
No effect to compute values: do it during render.
No effect for user actions: do it in the handler. Reset with a key.
useEffectEvent (React 19.2) reads the latest values without being a dependency.
FlashcardsInteractive · disabled in the PDF
What is an effect for?
Keeping an external system in step with what is on screen.
Dependency array
The values an effect reads; it re-runs when one of them changes.
Cleanup
The function an effect returns; runs before the next run and on unmount.
Mount and unmount
A component appearing on screen, and leaving it.
useEffectEvent
A function for effects that sees the latest values and isn’t a dependency.
Module 12: Refs and the DOM
React114 / 204
Module 12 · Refs and the DOM
A ref is a box React keeps but doesn’t watch
useRef gives you an object with one field, current. React keeps the same object across renders, like state. Unlike state, changing ref.current doesn’t trigger a render. Use a ref for values your handlers and effects need but the screen doesn’t show.
Sometimes you need the real DOM node: to focus an input, scroll to a row, or measure a size. Pass a ref to a built-in element’s ref prop. After the commit, React puts the node in ref.current; when the element goes away, it sets it back to null.
TSX
1functionAddTodo(){2const input =useRef<HTMLInputElement>(null)34functionhandleAdded(){5// …add the to-do, then put the cursor back6 input.current?.focus()7}89return<inputref={input}/>10}
?. (optional chaining) calls focus only if current isn’t null.
Clicking Clear empties the search, but the cursor is left on the button, so you have to click the field again to type. Focusing a field is something only the DOM can do.
Code challengeInteractive · disabled in the PDF
Make a ref with useRef, attach it to the input, and in clearSearch call focus() on the node, so after Clear the Search recipes field has the cursor.
TSX
1import{ useRef, useState }from'react'23exportdefaultfunctionSearchBar(){4const[query, setQuery]=useState('')5// TODO: a ref for the input67functionclearSearch(){8setQuery('')9// TODO: put the cursor back in the field10}1112return(13<div>14<label>15 Search recipes{' '}16<input17value={query}18onChange={(e)=>setQuery(e.target.value)}19/>20</label>21<buttononClick={clearSearch}>Clear</button>22</div>23)24}
React118 / 204
Module 12 · Refs and the DOM
In React 19, ref is just a prop
To let a parent reach an input inside your own component, accept ref like any other prop and pass it on. Before React 19 this needed a wrapper called forwardRef; you will still see it in older code, but new code doesn’t need it.
TSX
1typeFieldProps=React.ComponentProps<'input'>&{2 label:string3}45functionTextField({ label, ref,...rest }:FieldProps){6const id =useId()7return(8<>9<labelhtmlFor={id}>{label}</label>10<inputid={id}ref={ref}{...rest}/>11</>12)13}
ComponentProps<'input'> is every prop an input accepts, ref included. & adds label.
React119 / 204
Module 12 · Refs and the DOM
Ref callbacks can clean up
A ref prop can also be a function. React calls it with the node when the element appears. Since React 19 it may return a cleanup, which React calls when the element goes away. Handy for a ref per row in a list.
Refs to DOM nodes are an escape hatch: a way out of React’s model for the rare cases it doesn’t cover. Focus, scrolling, measuring, selecting text and handing a node to a non-React library are fine. Changing what React draws is not: React would overwrite it, or get confused by it, on the next commit.
Find the bugInteractive · disabled in the PDF
This works once, then strange things happen when the list changes. Which line is the problem?
TSX
React121 / 204
Module 12 · Refs and the DOM
Expose a small API with useImperativeHandle
Sometimes a parent shouldn’t get the whole DOM node, only a couple of actions. useImperativeHandle lets a component decide what its ref gives the parent. Use it sparingly: most things are better as props.
A parent wants a modal to be open or closed. Prop or ref?
A parent wants a modal to be open or closed. Prop or ref?
React122 / 204
Module 12 · Refs and the DOM
Recap
useRef keeps a value across renders without rendering when it changes.
Don’t read or write refs during render; use them in handlers and effects.
Pass a ref to an element’s ref prop to get its DOM node after commit.
In React 19, ref is a normal prop for your components; no forwardRef.
Ref callbacks can return a cleanup.
Use DOM refs for focus, scroll and measuring, never to change what React draws.
FlashcardsInteractive · disabled in the PDF
useRef(initial)
An object { current } kept across renders; changing it doesn’t render.
When is a DOM ref filled?
After React commits the element; null before and after it exists.
Escape hatch
A way to step outside React’s model, for what it can’t express.
forwardRef
The pre-19 way to pass a ref through a component. Not needed in React 19.
useImperativeHandle
Chooses what a component’s ref exposes to its parent.
Module 13: Reducers and context
React123 / 204
Module 13 · Reducers and context
When updates are spread across many handlers
The to-do app now adds, ticks, renames, deletes and clears. Each is a handler with its own setTodos(…), spread across the component. It works, but to see every way the to-dos can change you must read the whole file.
Lines 1-4: An action is a plain object describing what happened. type names it; the rest is its details. Name actions in the past tense: they are events, not commands.
Lines 6: The reducer takes the current state and one action.
Lines 7-16:switch picks a branch by action.type. Each returns a new state, built without mutating, as in the updating-state module.
Note. A reducer must be pure, like a component: no timers, no network, no Math.random() inside. (Strictly, Date.now() here should move to the handler that dispatches; it is kept short for the slide.)
React125 / 204
Module 13 · Reducers and context
useReducer: dispatch actions instead of setting state
TSX
1import{ useReducer }from'react'23functionTodoApp(){4const[todos, dispatch]=useReducer(todosReducer, starterTodos)56return(7<>8<AddTodoonAdd={(text)=>dispatch({type:'added', text })}/>9<TodoList10todos={todos}11onToggle={(id)=>dispatch({type:'toggled', id })}12onDelete={(id)=>dispatch({type:'deleted', id })}13/>14</>15)16}
useReducer(reducer, initial) returns the state and dispatch. Calling dispatch(action) queues the action; on the next render React runs the reducer to get the new state. It is useState with the update logic moved into one testable function.
Predict the outputInteractive · disabled in the PDF
The shop’s buttons already dispatch actions. Only the reducer is missing, so nothing happens yet.
Code challengeInteractive · disabled in the PDF
Write cartReducer. added adds a line { name, qty: 1 }, or adds one to the qty of the line with that name; cleared empties the cart. Return new arrays and objects.
TSX
1import{ useReducer }from'react'23typeLine={ name:string; qty:number}4typeAction={ type:'added'; name:string}|{ type:'cleared'}56functioncartReducer(cart:Line[], action:Action):Line[]{7// TODO: handle 'added' and 'cleared'8return cart9}1011exportdefaultfunctionShop(){12const[cart, dispatch]=useReducer(cartReducer,[])13constadd=(name:string)=>dispatch({type:'added', name })14constclear=()=>dispatch({type:'cleared'})15return(16<div>17<buttononClick={()=>add('Apple')}>Add an apple</button>18<buttononClick={()=>add('Pear')}>Add a pear</button>19<buttononClick={clear}>Clear</button>20<ul>21{cart.map((l)=><likey={l.name}>{l.name} × {l.qty}</li>)}22</ul>23</div>24)25}
React127 / 204
Module 13 · Reducers and context
useState or useReducer?
Pick
When
useState
Independent values with simple updates: a draft, an open flag
useReducer
Several kinds of update to the same data, or updates that change several fields together
Either
Convert later if it grows: the rest of the component barely changes
Fill the blanksInteractive · disabled in the PDF
Add a “cleared” action that removes every finished to-do.
TypeScript
1case'cleared':2return todos.((t)=>)
React128 / 204
Module 13 · Reducers and context
Passing props through layers that don’t use them
Some data is needed far down the tree: the theme, the signed-in user, the chosen language. Passing it as a prop through every layer in between, even layers that never use it, is called prop drilling. It makes those middle components noisy and hard to move.
Try children first. Often the fix is composition: have App render <Page><TodoList theme={theme} /></Page>, so Page takes children and never sees the theme. Reach for context when that doesn’t fit.
Lines 4:createContext makes a context. The argument is the default, used when no provider is above.
Lines 9-11:Provide a value to everything inside. In React 19 you render the context itself; older code writes <ThemeContext.Provider value={…}>.
Lines 16:useContext reads the value from the nearest provider above, however deep. When that value changes, this component renders again.
React130 / 204
Module 13 · Reducers and context
Try it: read a context
App provides a language to everything inside it, and its button switches it. The greetings, two levels down, don’t read it yet.
Code challengeInteractive · disabled in the PDF
In Greeting, read LangContext with useContext, and show Bonjour when it is fr, Hello otherwise. Don’t pass any new props.
TSX
1import{ createContext, useContext, useState }from'react'23typeLang='en'|'fr'4const LangContext =createContext<Lang>('en')56functionGreeting(){7// TODO: read the language from LangContext8return<p>Hello</p>9}1011functionCard(){12return<section><Greeting/><Greeting/></section>13}1415exportdefaultfunctionApp(){16const[lang, setLang]=useState<Lang>('en')17return(18<LangContextvalue={lang}>19<buttononClick={()=>setLang('fr')}>Français</button>20<Card/>21</LangContext>22)23}
React131 / 204
Module 13 · Reducers and context
Together: a provider for the to-dos
A reducer and context combine into a small store for one part of the app. One component owns the state; any component inside can read it or dispatch. Wrap the reading in custom Hooks (next module) so callers don’t touch the contexts directly.
src/TodosProvider.tsx
1const TodosContext =createContext<Todo[]>([])2const DispatchContext =createContext<React.Dispatch<Action>>(3()=>{}4)56exportfunctionTodosProvider({ children }:{7 children:React.ReactNode8}){9const[todos, dispatch]=useReducer(todosReducer,[])10return(11<TodosContextvalue={todos}>12<DispatchContextvalue={dispatch}>13{children}14</DispatchContext>15</TodosContext>16)17}1819exportconstuseTodos=()=>useContext(TodosContext)20exportconstuseDispatch=()=>useContext(DispatchContext)
dispatch never changes, so components that only dispatch don’t render when the to-dos do.
React132 / 204
Module 13 · Reducers and context
Context is not free: use it for wide, slow-changing data
Every component that reads a context renders again whenever its value changes. A context holding something that changes on every key press makes every reader render on every key press. And a new object as the value, value={{ todos, filter }}, is “changed” on every render of the provider.
QuizInteractive · disabled in the PDF
Which are good fits for context? Pick all.
Which are good fits for context? Pick all.
React133 / 204
Module 13 · Reducers and context
Recap
A reducer is a pure function (state, action) => next state.
useReducer returns state and dispatch; handlers dispatch what happened.
Prop drilling is passing data through layers that don’t use it. Try children first.
Context: createContext, provide with <Ctx value={…}>, read with useContext.
Context readers render when the value changes: keep it wide and slow-changing.
Reducer plus context makes a small store for one part of the app.
FlashcardsInteractive · disabled in the PDF
Reducer
A pure function from the current state and an action to the next state.
Action
A plain object describing what happened, with a type.
dispatch
Sends an action to the reducer. Its identity never changes.
Prop drilling
Threading a prop through components that only pass it on.
Context
A way for a parent to make a value readable by every component below.
Provider in React 19
<MyContext value={…}>; before 19, <MyContext.Provider>.
Module 14: Custom Hooks and fetching data
React134 / 204
Module 14 · Custom Hooks and fetching data
A custom Hook is a function that calls Hooks
When two components need the same stateful logic, move it into a function whose name starts with use. That is a custom Hook. Inside it you can call useState, useEffect and other Hooks, exactly as in a component.
First, a way to save to-dos so a reload doesn’t lose them: localStorage, a small store of text in the browser that is kept between visits. JSON turns data into text (JSON.stringify) and back (JSON.parse).
<T> makes it generic: it works for any type of value, and keeps that type.
React135 / 204
Module 14 · Custom Hooks and fetching data
Use it like a built-in Hook
TSX
1functionTodoApp(){2const[todos, setTodos]=useStoredState<Todo[]>('todos',[])3const[filter, setFilter]=useStoredState<Filter>('filter','all')4// …the rest is unchanged5}
QuizInteractive · disabled in the PDF
Two different components both call useStoredState('todos', []). Do they share one list in memory?
Two different components both call useStoredState('todos', []). Do they share one list in memory?
Note. Calling localStorage while rendering only works in the browser. If the page is first drawn on a server (later modules), read it in an effect instead.
React136 / 204
Module 14 · Custom Hooks and fetching data
Rules for your own Hooks
Name it use plus a capital: useStoredState, useOnline. The linter uses the name to check the rules of Hooks.
Call Hooks only at the top level of components and other Hooks.
Only make it a Hook if it calls Hooks. A plain helper, like formatDate, stays a plain function.
Name it for what it does (useOnlineStatus), not how (useWindowListener).
Find the bugInteractive · disabled in the PDF
This throws an error about Hooks when you click. Which line?
TSX
React137 / 204
Module 14 · Custom Hooks and fetching data
fetch asks a server for data
Now a recipe browser: type a word, see matching recipes from a server. The browser’s fetch sends a request and gives back a promise: a value that will arrive later. await (inside an async function) waits for it without freezing the page.
encodeURIComponent makes the text safe inside an address. throw reports an error to whoever called.
Request
A message from the browser to a server, asking for something.
Promise
A value that isn’t ready yet. await waits for it.
API
Here: addresses on a server that answer with data, often JSON, instead of pages.
React138 / 204
Module 14 · Custom Hooks and fetching data
Show loading, error and data
Data from a server takes time and can fail. The screen must handle three cases. Fetching to keep a component in step with its inputs is a sync with an outside system, so it goes in an effect.
.then(onDone, onFail) runs one of two functions when the promise settles.
React139 / 204
Module 14 · Custom Hooks and fetching data
Ignore answers that arrive too late
Type “pa”, then “pasta”. Two requests are now in flight, and the network doesn’t promise they come back in order. If “pa” answers last, the screen shows the wrong recipes. This is a race condition. The fix is a cleanup that marks the old request as stale.
Find the bugInteractive · disabled in the PDF
Which line should change so that a late answer to an old query is ignored?
TSX
Cancel, too. An AbortController can cancel the request itself: pass controller.signal to fetch and call controller.abort() in the cleanup. That saves bandwidth, but you still need to handle the AbortError it throws.
Typing “lasagne” fires seven requests, one per letter. What is the problem, and a common fix?
Typing “lasagne” fires seven requests, one per letter. What is the problem, and a common fix?
React141 / 204
Module 14 · Custom Hooks and fetching data
Try it: a Hook that loads recipes
There is no network here, so searchRecipes pretends: it answers after 100 ms, and fails for the query error. The component is done; the Hook it calls always says “loading”.
Code challengeInteractive · disabled in the PDF
Finish useRecipes: keep recipes and error in state, and in an effect call searchRecipes(query), storing the result or the error. Ignore answers that arrive after the query changed.
TSX
1import{ useEffect, useState }from'react'23const all =['Pasta bake','Papaya salad','Lemon cake']4constsearchRecipes=(q:string)=>5newPromise<string[]>((resolve, reject)=>{6setTimeout(()=>{7if(q ==='error')reject(newError('The kitchen is closed'))8resolve(all.filter((r)=> r.toLowerCase().includes(q)))9},100)10})1112typeResult={ recipes:string[]|null; error:Error|null}1314functionuseRecipes(query:string):Result{15// TODO: state for the recipes and the error, and an effect16return{recipes:null,error:null}17}1819exportdefaultfunctionRecipes({ query ='pa'}){20const{ recipes, error }=useRecipes(query)21if(error)return<prole="alert">{error.message}</p>22if(!recipes)return<p>Loading…</p>23return<ul>{recipes.map((r)=><likey={r}>{r}</li>)}</ul>24}
React142 / 204
Module 14 · Custom Hooks and fetching data
In real apps, use a data library or your framework
Fetching in effects works, and it is good to understand. But real apps also want a cache (keep answers so going back is instant), shared requests when two components ask for the same thing, retries, and refreshing stale data. Writing that yourself is a lot.
Option
What it gives you
A data library, e.g. TanStack Query or SWR
Caching, sharing, retries and loading states behind one Hook
A framework’s data loading
Data loaded with the page, often on the server, before it renders
Suspense with use
React 19’s way to wait for a promise while rendering; a later module
Tip. Whatever you pick, wrap it in your own Hook (useRecipes). Components then don’t care how the data arrives, and you can switch later.
React143 / 204
Module 14 · Custom Hooks and fetching data
Recap
A custom Hook is a use… function that calls Hooks. It shares logic, not state.
localStorage plus JSON saves small data between visits.
fetch returns a promise; await or .then gets the result.
Handle loading, error, empty and data, every time.
Guard against races with an ignore flag set in the cleanup.
For real apps, prefer a data library or your framework’s loading.
FlashcardsInteractive · disabled in the PDF
Custom Hook
A function named use… that calls other Hooks to package logic.
localStorage
Text saved in the browser per site, kept between visits.
Promise
A value that will arrive later, or fail.
Race condition
A bug where the result depends on which of two things finishes first.
Debounce
Waiting until input pauses before acting on it.
Cache
Kept copies of earlier answers, so asking again is instant.
Module 15: Accessible components
React144 / 204
Module 15 · Accessible components
Accessibility means everyone can use it
Accessibility (often written a11y: “a”, 11 letters, “y”) means people can use your app however they use a computer: with a mouse, only a keyboard, a screen reader, voice control, zoomed to 200%, or in bright sunlight. React doesn’t make an app accessible or inaccessible; the elements and attributes you render do.
Assistive technology
Software or hardware that helps people use computers, like screen readers.
Accessible name
What assistive technology calls an element: its label, its text, or aria-label.
Focus
Which element receives the keyboard right now. Tab moves it.
Note. In many countries, accessibility is also a legal requirement. The usual standard is WCAG, the Web Content Accessibility Guidelines.
React145 / 204
Module 15 · Accessible components
Use the real element: a button is a button
A <button> can be focused with Tab, pressed with Enter or Space, and is announced as a button. A <div onClick> does none of that: keyboard users can’t reach it, and screen readers don’t know it does anything. Built-in elements give you all of it for free.
A button with only an icon has no text, so a screen reader says “button” and nothing else. Give it a name with aria-label. Images need alt text describing them, or alt="" if they are only decoration.
aria-hidden hides the icon itself from screen readers, since the button already has a name.
Find the bugInteractive · disabled in the PDF
A screen reader announces one of these as just “checkbox”. Which line needs fixing?
TSX
React147 / 204
Module 15 · Accessible components
Try it: name every control
The tests find things the way a screen reader does: by role and by name. Right now each checkbox is just “checkbox” and each delete button is just “×”.
Code challengeInteractive · disabled in the PDF
Wrap each checkbox and its text in a <label>, so the box is named after the to-do, and give the × button the name Delete plus the to-do’s text (Delete Buy milk).
TSX
1typeTodo={ id:number; text:string; done:boolean}23functionTodoRow({ todo }:{ todo:Todo}){4// TODO: name the checkbox and the delete button5return(6<li>7<inputtype="checkbox"defaultChecked={todo.done}/>8<span>{todo.text}</span>9<buttontype="button">×</button>10</li>11)12}1314exportdefaultfunctionList(){15return(16<ul>17<TodoRowtodo={{id:1,text:'Buy milk',done:false}}/>18<TodoRowtodo={{id:2,text:'Walk the dog',done:true}}/>19</ul>20)21}
React148 / 204
Module 15 · Accessible components
ARIA describes what HTML can’t
ARIA (Accessible Rich Internet Applications) is a set of attributes that tell assistive technology about roles and states HTML has no tag for. In JSX they keep their dashes: aria-pressed, aria-expanded. Its first rule: if a real HTML element does the job, use that instead.
A disclosure: the button says whether the panel it controls is open.
QuizInteractive · disabled in the PDF
You need a toggle between “all” and “done”. What is the best first choice?
You need a toggle between “all” and “done”. What is the best first choice?
React149 / 204
Module 15 · Accessible components
Try it: a disclosure that says if it’s open
Code challengeInteractive · disabled in the PDF
The Ingredients button already shows and hides the list. Add aria-expanded so it tells a screen reader whether the list is open, and aria-controls pointing at the list’s id.
When a to-do is added or a search finishes, a sighted person sees it. A screen-reader user hears nothing unless you tell them. A live region is an element whose changes are read out. role="status" is polite (waits its turn); role="alert" interrupts, for errors.
Keep the live region on screen and change its text. A region that appears already filled may not be read.
React151 / 204
Module 15 · Accessible components
Manage focus when the page changes under it
If the element with focus disappears, focus falls back to the top of the page, and a keyboard user loses their place. After deleting a to-do, move focus somewhere sensible, like the next row or the input. Refs (the refs module) make this possible.
TSX
1functionhandleDelete(id:number){2dispatch({type:'deleted', id })3 newTodoInput.current?.focus()4}
Dialogs: use the dialog element. The HTML <dialog> element, opened with its showModal() method from a ref, traps focus inside, closes on Escape, and returns focus when it closes. Hand-made modals have to rebuild all of that.
Watch out. Never remove the focus outline (outline: none) without a clear replacement. It is how keyboard users see where they are. :focus-visible in CSS shows it for keyboards and hides it for mouse clicks.
React152 / 204
Module 15 · Accessible components
Check: testing accessibility
Put in orderInteractive · disabled in the PDF
A cheap, effective routine for checking a new screen. Put it in order, from quickest to most thorough.
1.Use it with a screen reader (VoiceOver, NVDA or TalkBack)
2.Let the linter flag issues as you write (eslint-plugin-jsx-a11y)
3.Put the mouse away and use the screen with only the keyboard
4.Run an automated checker such as axe or Lighthouse in the browser
QuizInteractive · disabled in the PDF
An automated checker reports zero issues. Is the screen accessible?
An automated checker reports zero issues. Is the screen accessible?
React153 / 204
Module 15 · Accessible components
Recap
Accessibility comes from what you render, not from React itself.
Use real elements: button for actions, a for navigation, headings in order.
Every control needs an accessible name: a label, text, or aria-label.
ARIA adds states HTML lacks (aria-pressed, aria-expanded); HTML first.
Announce out-of-sight changes with role="status" or role="alert".
Move focus on purpose when its element disappears; keep focus outlines.
FlashcardsInteractive · disabled in the PDF
a11y
Accessibility: “a”, 11 letters, “y”.
Accessible name
What assistive technology announces for an element.
First rule of ARIA
If a native HTML element does the job, use it instead.
Live region
An element whose text changes are read aloud: role="status" or role="alert".
:focus-visible
A CSS selector for focus that should be shown, typically from the keyboard.
Module 16: Performance
React154 / 204
Module 16 · Performance
Measure before you optimise
Most React apps are fast without any tuning. When something feels slow, find out what is slow before changing code. The React Developer Tools have a Profiler tab: record while you do the slow thing, and it shows each commit, which components rendered, how long each took, and why.
Reproduce the slowness, ideally with a production build (npm run build then npm run preview): development mode is much slower.
Record it in the Profiler and find the commits that take long. The Profiler works in development, or in React’s profiling build (react-dom/profiling) for production timings: a normal production build can’t be profiled.
Find the components that rendered without needing to, or that are slow themselves.
Fix one thing, then measure again.
Tip. Turn on “Highlight updates when components render” in the developer tools settings. Rows that flash when you type in a different box are a good lead.
React155 / 204
Module 16 · Performance
Why a component renders
A component renders again for one of three reasons: its own state changed, a context it reads changed, or its parent rendered. The third surprises people: by default, when a parent renders, all of its children render too, even if their props are identical.
Predict the outputInteractive · disabled in the PDF
You type in AddTodo’s input, whose draft is state in App. Which components render?
You type in AddTodo’s input, whose draft is state in App. Which components render?
React156 / 204
Module 16 · Performance
The cheapest fix: move state down
Before any memoisation, look at where the state lives. The draft is only used by AddTodo, so it belongs there. Then typing renders AddTodo alone, and the list never notices.
Children passed in don’t re-render. A component that takes children and changes its own state doesn’t re-render those children: they were created by its parent, which didn’t render. Wrapping expensive content as children of a stateful component is a free optimisation.
React157 / 204
Module 16 · Performance
memo skips a render when props are the same
memo wraps a component so that, when its parent renders, React first compares the new props with the old ones using Object.is, one by one. If all are the same, it reuses the last result. Memoisation means remembering a result so you don’t compute it again.
Now ticking one to-do can render only that row: the others get the same todo objects, because the update made a new object for the changed one only. That is the immutability rule paying off. Their onToggle must stay the same function too, which is the next slide.
React158 / 204
Module 16 · Performance
Try it: stop the chart redrawing
The chart counts its own renders on screen, so you can watch them. Type in the note: every key press draws the chart again, though nothing it shows has changed.
Code challengeInteractive · disabled in the PDF
Make typing in Note leave the chart alone, so it stays at Chart drawn 1 times. Either move the note’s state down into a NoteField component, or wrap SlowChart in memo.
TSX
1import{ memo, useRef, useState }from'react'23functionSlowChart(){4// A render counter, for this lesson only: real code5// mustn't change a ref while rendering.6const draws =useRef(0)7 draws.current++8return<p>Chart drawn {draws.current} times</p>9}1011exportdefaultfunctionDashboard(){12const[note, setNote]=useState('')13return(14<div>15<label>16 Note{' '}17<inputvalue={note}onChange={(e)=>setNote(e.target.value)}/>18</label>19<SlowChart/>20</div>21)22}
React159 / 204
Module 16 · Performance
New objects and functions defeat memo
An object, array or function written inside a component is a new one on every render. Object.is says new and old differ, so memo renders anyway. useCallback keeps the same function between renders; useMemo keeps the result of a calculation. Each re-runs only when its dependencies change.
Find the bugInteractive · disabled in the PDF
TodoItem is wrapped in memo, yet every row still renders when the draft changes. Which line is the cause?
The React Compiler is a build tool plugin from the React team (version 1.0 since October 2025). It reads your components and adds the memoisation automatically, at a finer grain than you would by hand. With it, most memo, useMemo and useCallback calls become unnecessary.
It only works with code that follows the rules: pure renders, no mutation of props or state, Hooks at the top level.
When it can’t prove a component is safe, it leaves that component alone rather than break it.
The eslint-plugin-react-hooks rules flag code it can’t compile. Fixing them makes code better anyway.
QuizInteractive · disabled in the PDF
Why does the compiler need components to be pure and state immutable?
Why does the compiler need components to be pure and state immutable?
React161 / 204
Module 16 · Performance
Long lists and big bundles
Two problems memo can’t fix. A list of 10,000 rows is slow because the DOM is huge: virtualise it, drawing only the rows in view (libraries such as TanStack Virtual do this). And an app that downloads all its code at once starts slowly: code-split it, loading parts only when needed.
lazy loads Stats.tsx the first time it renders. Suspense shows the fallback meanwhile; more next module.
React162 / 204
Module 16 · Performance
Check: a slow screen
Put in orderInteractive · disabled in the PDF
Typing in the search box of the recipe browser feels laggy. Put a sound approach in order.
1.Measure again to confirm it helped
2.Reproduce it in a production build
3.Record typing in the Profiler and find what renders on each key press
4.If it is still slow, memoise the expensive part or defer it
5.Move state down or restructure so less of the tree depends on it
React163 / 204
Module 16 · Performance
Recap
Measure with the Profiler before changing code; trust timings from a production or profiling build.
A component renders when its state, a context it reads, or its parent changes.
Moving state down and passing children are the cheapest fixes.
memo skips renders with equal props; useCallback and useMemo keep props equal.
The React Compiler adds memoisation automatically for code that follows the rules.
Virtualise very long lists; code-split with lazy and Suspense.
FlashcardsInteractive · disabled in the PDF
Profiler
A developer tools tab that records commits and what rendered, and why.
memo(Component)
Skips re-rendering when every prop is Object.is-equal to last time.
useCallback(fn, deps)
The same function between renders until a dependency changes.
useMemo(calc, deps)
A remembered result of calc until a dependency changes.
React Compiler
A build plugin that memoises components and values automatically.
Virtualisation
Rendering only the rows of a long list that are on screen.
Module 17: Suspense and transitions
React164 / 204
Module 17 · Suspense and transitions
Some updates are urgent, some can wait
Typing in a box must feel instant. Showing 500 matching recipes can take a moment. Since React 18, rendering is concurrent: React can start rendering an update, pause it to handle something more urgent, and come back, or throw it away if it is no longer needed. You tell React which updates can wait.
Concurrent rendering
React preparing a new screen in the background, interruptible, while the current one stays usable.
Transition
An update marked as not urgent, which React may interrupt.
Suspense
A boundary that shows a fallback while something inside isn’t ready.
React165 / 204
Module 17 · Suspense and transitions
Suspense shows a fallback while children wait
Wrap part of the tree in <Suspense fallback={…}>. If a component inside suspends (says “I can’t render yet, I’m waiting”), React shows the fallback in place of the whole boundary, then swaps in the real content when it is ready.
Watch out. Fetching inside an effect does not suspend: the component renders, then fetches. Suspense only works with sources that are built for it.
React166 / 204
Module 17 · Suspense and transitions
Try it: a boundary in the right place
BookList reads a promise with use (React 19, the next module), so it suspends until the books arrive. With no boundary, the whole page waits blank. The promise is made once, outside any component, so every render reads the same one.
Code challengeInteractive · disabled in the PDF
Wrap BookList, and only BookList, in <Suspense> with the fallback <p>Loading books…</p>, so the heading shows at once and the list replaces the fallback when it is ready.
TSX
1import{ Suspense, use }from'react'23constfetchBooks=()=>4newPromise<string[]>((resolve)=>5setTimeout(()=>resolve(['Dune','Emma','Beloved']),200)6)7const booksPromise =fetchBooks()89functionBookList(){10const books =use(booksPromise)11return<ul>{books.map((b)=><likey={b}>{b}</li>)}</ul>12}1314exportdefaultfunctionShop(){15// TODO: a Suspense boundary around the list16return(17<main>18<h1>Bookshop</h1>19<BookList/>20</main>21)22}
React167 / 204
Module 17 · Suspense and transitions
Where you put boundaries decides what the user sees
React shows the fallback of the nearest boundary above whatever suspended. One boundary around the page gives one spinner for everything. A boundary per section lets each part appear when it is ready. Design boundaries the way you would design loading states with a designer.
App
Header
Suspensepage skeleton
RecipeDetails
Suspensereviews spinner
Reviews
QuizInteractive · disabled in the PDF
In the tree above, Reviews suspends but RecipeDetails is ready. What does the user see?
In the tree above, Reviews suspends but RecipeDetails is ready. What does the user see?
React168 / 204
Module 17 · Suspense and transitions
useTransition marks an update as not urgent
useTransition returns isPending and startTransition. State updates inside startTransition are transitions: React renders them in the background, keeps the current screen responsive, and drops the work if a newer update arrives.
Transitions don’t hide content you already see. If a transition makes something suspend, React keeps showing the old tab (with isPending true) instead of swapping in the fallback. Without the transition, the panel would flash to a spinner on every switch.
React169 / 204
Module 17 · Suspense and transitions
Check: which updates go in a transition
QuizInteractive · disabled in the PDF
Which update must not be put in a transition?
Which update must not be put in a transition?
React170 / 204
Module 17 · Suspense and transitions
useDeferredValue lets a slow part lag behind
When you don’t control the update, defer the value instead. useDeferredValue(query) returns the old value at first; React then renders the new one in the background. The box updates on every key; the results catch up when they can.
TSX
1functionRecipeSearch(){2const[query, setQuery]=useState('')3const deferred =useDeferredValue(query)4const stale = query !== deferred56return(7<>8<inputvalue={query}onChange={(e)=>setQuery(e.target.value)}/>9<divstyle={{opacity: stale ?0.6:1}}>10<Suspensefallback={<p>Searching…</p>}>11<Resultsquery={deferred}/>12</Suspense>13</div>14</>15)16}
While old results are shown, they are dimmed. Suspense only falls back on the very first search.
Error boundaries catch what goes wrong while rendering
If a component throws while rendering (a bug, or a failed promise read with use), React removes the whole tree, leaving a blank page. An error boundary catches the error and shows a fallback for that part instead. They are written as class components, so most apps use the small react-error-boundary package.
TSX
1import{ ErrorBoundary }from'react-error-boundary'23<ErrorBoundaryfallback={<p>Recipes could not be loaded.</p>}>4<Suspensefallback={<p>Loading…</p>}>5<Resultsquery={query}/>6</Suspense>7</ErrorBoundary>
Loading goes to Suspense, failure goes to the error boundary: a matched pair.
Note. Error boundaries don’t catch errors in event handlers or in code after an await: those don’t happen during rendering. Catch them yourself (try/catch, or the second function given to .then) and keep the error in state, as in the fetching module. One exception comes next module: an error thrown inside an Action started by useTransition or a form goes to the nearest error boundary.
React172 / 204
Module 17 · Suspense and transitions
Recap
Concurrent rendering lets React prepare updates in the background and interrupt them.
<Suspense fallback> shows the fallback of the nearest boundary while something inside suspends.
Only Suspense-aware sources suspend: lazy, use(promise), some libraries.
startTransition marks updates as not urgent and keeps old content instead of a fallback.
useDeferredValue lets a slow part render with an older value.
Error boundaries catch rendering errors; pair them with Suspense.
FlashcardsInteractive · disabled in the PDF
Suspend
A component signalling that it can’t render yet because it is waiting.
useTransition()
Returns [isPending, startTransition] to mark updates as not urgent.
useDeferredValue(v)
A copy of v that may lag behind, rendered in the background.
Error boundary
A component that catches errors thrown while rendering below it.
Why not wrap input state in a transition?
Typing must update at once; defer the expensive consumer instead.
Module 18: Actions, use and the server
React173 / 204
Module 18 · Actions, use and the server
An Action is an async transition
In React 19, the function you pass to startTransition may be async. React calls such a function an Action. isPending stays true for the whole Action, including the waiting, so a save button can show its own progress without a separate loading state.
TSX
1functionSaveButton({ todos }:{ todos:Todo[]}){2const[isPending, startTransition]=useTransition()3const[error, setError]=useState<string|null>(null)45functionsave(){6startTransition(async()=>{7const failed =awaitsaveTodos(todos)8startTransition(()=>setError(failed))9})10}1112return(13<>14<buttononClick={save}disabled={isPending}>15{isPending ?'Saving…':'Save'}16</button>17{error &&<prole="alert">{error}</p>}18</>19)20}
Note. A state update made after an await inside an Action needs its own startTransition to count as part of it, as on line 8. The React docs call this a current limitation. And if an Action throws, React shows the nearest error boundary, so an error you want to show in place, like this one, comes back as a value or is caught.
React174 / 204
Module 18 · Actions, use and the server
Pass a function to a form’s action
A <form> can take a function as its action. On submit, React calls it with the form’s FormData, inside a transition. No preventDefault, no controlled inputs. When the Action finishes, React resets the form’s uncontrolled fields.
TSX
1typeProps={onAdd:(text:string)=>Promise<void>}23functionAddTodo({ onAdd }:Props){4asyncfunctionadd(data:FormData){5const text =String(data.get('text')).trim()6if(text)awaitonAdd(text)7}89return(10<formaction={add}>11<inputname="text"aria-label="New to-do"/>12<button>Add</button>13</form>14)15}
React175 / 204
Module 18 · Actions, use and the server
useActionState keeps the result of the last submit
WalkthroughInteractive · disabled in the PDF
TSX
1import{ useActionState }from'react'23typeResult={ error:string|null}45functionAddTodo(){6const[result, formAction, isPending]=useActionState(7async(previous:Result, data:FormData):Promise<Result>=>{8const text =String(data.get('text')).trim()9if(!text)return{error:'Write something first.'}10const problem =awaitcreateTodo(text)11return{error: problem }12},13{error:null}14)1516return(17<formaction={formAction}>18<inputname="text"aria-label="New to-do"/>19<buttondisabled={isPending}>Add</button>20{result.error &&<prole="alert">{result.error}</p>}21</form>22)23}
Lines 6: It returns the latest result, an action to give the form, and whether one is running.
Lines 7-12: Your function gets the previous result and the form data, and returns the next result: like a reducer, but async.
Lines 13: The result before anything is submitted.
Lines 17-20: Wire it up and show the error, if any.
QuizInteractive · disabled in the PDF
A function given to useActionState is written async (data: FormData) => …. What goes wrong?
A function given to useActionState is written async (data: FormData) => …. What goes wrong?
React176 / 204
Module 18 · Actions, use and the server
Try it: a form with an Action
A newsletter form using useActionState. The pretend server takes a second and refuses addresses without an @. Submit it a few times, empty and filled, and watch the button and the message.
PlaygroundInteractive · disabled in the PDF
TSX
1import{ useActionState }from'react'23constpause=(ms:number)=>newPromise((r)=>setTimeout(r, ms))45asyncfunctionsubscribe(previous:string, data:FormData){6const email =String(data.get('email')).trim()7awaitpause(1000)8if(!email.includes('@'))return'That doesn’t look like an email.'9return'Subscribed '+ email +'.'10}1112exportdefaultfunctionNewsletter(){13const[message, action, isPending]=useActionState(subscribe,'')14return(15<formaction={action}>16<label>17 Email <inputname="email"/>18</label>19<buttondisabled={isPending}>20{isPending ?'Subscribing…':'Subscribe'}21</button>22<prole="status">{message}</p>23</form>24)25}
Try pause(3000) to feel the wait, or disable the input too while isPending.
React177 / 204
Module 18 · Actions, use and the server
useFormStatus reads the form a component is inside
useFormStatus, from react-dom, tells a component whether the <form>around it is submitting. It is made for reusable buttons that don’t want a pending prop passed in.
Find the bugInteractive · disabled in the PDF
pending is always false here. Which line is the problem?
TSX
React178 / 204
Module 18 · Actions, use and the server
useOptimistic shows the result before the server answers
Waiting a second for a new to-do to appear feels slow. An optimistic update shows it straight away, as if the save had worked, and settles to the real data when the Action ends. If the save fails, the temporary item disappears by itself; catch the error in the Action to say so, or it goes to the nearest error boundary.
TSX
1functionTodos({ todos, addTodo }:Props){2const[shown, addShown]=useOptimistic(3 todos,4(current:Todo[], text:string)=>[5...current,6{id:-1, text,done:false,saving:true}7]8)910asyncfunctionadd(data:FormData){11const text =String(data.get('text'))12addShown(text)13awaitaddTodo(text)14}1516return<TodoFormaction={add}todos={shown}/>17}
Call the optimistic setter inside an Action, such as a form action. saving lets the row look provisional.
React179 / 204
Module 18 · Actions, use and the server
Try it: a like that lands at once
The like button waits for the pretend server (100 ms here, much longer on a bad network) before the count moves. With fail, the server refuses.
Code challengeInteractive · disabled in the PDF
Use useOptimistic(likes) so a click shows Like (11) at once, inside the Action, while the save runs. If the save fails, the button should settle back to Like (10), with Not saved shown.
TSX
1import{ startTransition, useOptimistic, useState }from'react'23constsaveLikes=(n:number, fail:boolean)=>4newPromise<number>((resolve, reject)=>5setTimeout(()=>(fail ?reject(newError()):resolve(n)),100)6)78exportdefaultfunctionLikeButton({ fail =false}){9const[likes, setLikes]=useState(10)10const[error, setError]=useState('')11// TODO: an optimistic count, shown on the button12functionlike(){13startTransition(async()=>{14try{15const saved =awaitsaveLikes(likes +1, fail)16startTransition(()=>setLikes(saved))17}catch{18startTransition(()=>setError('Not saved'))19}20})21}22return(23<div>24<buttononClick={like}>Like ({likes})</button>25<p>{error}</p>26</div>27)28}
React180 / 204
Module 18 · Actions, use and the server
use reads a promise or a context while rendering
use is a new React API. Given a promise, it suspends until the promise settles, then returns its value; a rejection goes to the nearest error boundary. Given a context, it works like useContext. Unlike Hooks, it may be called inside if and loops.
TSX
1import{ use }from'react'23functionRecipe({ recipePromise }:{ recipePromise:Promise<Recipe>}){4const recipe =use(recipePromise)5return<h2>{recipe.title}</h2>6}78// a Server Component, a cache or a framework starts the request:9<Suspensefallback={<p>Loading…</p>}>10<ReciperecipePromise={fetchRecipe(id)}/>11</Suspense>
Don’t create the promise in the component that reads it. A promise made during render is a new promise every render, so it suspends forever. The promise must come from somewhere stable: a cache, a data library, a framework, or a Server Component.
React181 / 204
Module 18 · Actions, use and the server
Server Components render on the server only
Server Components are components that run on the server, ahead of time or per request, and send their result to the browser instead of their code. They can be async, read a database or files directly, and add nothing to the JavaScript the browser downloads. They need a framework that supports them, such as Next.js.
Server Component
Client Component
Runs
On the server
In the browser (and often on the server first)
State, effects, handlers
No
Yes
Can be async, read a database
Yes
No
Marked with
The default in such frameworks
'use client' at the top of the file
A file starting with 'use server' holds Server Functions: functions the browser can call, which run on the server. They work well as form actions.
React182 / 204
Module 18 · Actions, use and the server
Check: server or client
QuizInteractive · disabled in the PDF
In a framework with Server Components, which parts of the recipe page must be Client Components? Pick all.
In a framework with Server Components, which parts of the recipe page must be Client Components? Pick all.
Also new in React 19. You can render <title> and <meta> in any component and React moves them into the document’s head. Ref callbacks may return cleanups, and ref is a prop (the refs module).
React183 / 204
Module 18 · Actions, use and the server
Recap
An Action is an async function run in a transition; isPending covers its whole run.
<form action={fn}> calls fn(formData) and resets the form afterwards.
useActionState(fn, initial): fn(previous, formData) returns the next result.
useFormStatus (from react-dom) works only inside the form it reports on.
useOptimistic shows a temporary result until the Action ends.
use(promise) suspends; the promise must come from somewhere stable.
Server Components run only on the server; 'use client' marks interactive ones.
FlashcardsInteractive · disabled in the PDF
Action
A function, often async, run inside a transition.
useActionState returns
[result, formAction, isPending]
Where does useFormStatus work?
In a component rendered inside the <form>.
Optimistic update
Showing the expected result before the server confirms it.
use
Reads a promise (suspending) or a context; allowed inside if.
Server Component
A component that runs only on the server and ships no code to the browser.
Module 19: Under the hood
React184 / 204
Module 19 · Under the hood
Every update has a render phase and a commit phase
In the render phase React calls components and works out what changed. It touches nothing the user can see, so it can be paused, restarted or thrown away. In the commit phase React applies the changes to the DOM in one go and runs effects. Commit is never interrupted, so the screen is never half-updated.
Commit phasechange the DOM, attach refs, run layout effects
Browser paintsthe user sees the new screen
Passive effectsuseEffect runs
Why purity is non-negotiable. Because the render phase may run a component several times, or run it and discard the result, any side effect in render can happen too often or for a screen that never appears. Strict Mode’s double render simulates exactly this.
React185 / 204
Module 19 · Under the hood
React keeps a tree of fibers, one per component
Behind your component tree, React keeps its own tree of objects called fibers. A fiber records a component’s type, key, props, its place among siblings, and its Hooks’ data as a list, in call order. That list is why Hooks must be called in the same order every render: React reads slot 1, slot 2, slot 3 and matches them up.
Skip slot 1 with an if and React would hand draft the value of editing.
React holds two versions of the tree: the one on screen, and a work-in-progress one being rendered. On commit, the work-in-progress tree becomes the current one.
React186 / 204
Module 19 · Under the hood
Reconciliation: comparing the new tree with the old
Reconciliation is how React compares the new elements with the existing fibers. A perfect comparison of two trees is very slow, so React uses a few simple rules that are right almost always.
Different type at the same position: throw the old subtree away (DOM and state) and build the new one.
Same type at the same position: keep the fiber and its state, and update its props.
Lists: match children by key rather than position, so moved items keep their state.
Predict the outputInteractive · disabled in the PDF
A to-do row is being edited. Then this condition flips. Is the draft kept?
A to-do row is being edited. Then this condition flips. Is the draft kept?
React187 / 204
Module 19 · Under the hood
The scheduler decides what to render first
Each update gets a priority, which React calls a lane. Clicks and typing get a high one; transitions a lower one. In a concurrent render, React works in small slices and checks between them whether something more urgent is waiting. If so, it sets the transition aside, handles the urgent update, and restarts the transition on the new state.
React188 / 204
Module 19 · Under the hood
Check: the order of an update
Put in orderInteractive · disabled in the PDF
A state change updates a component that has a ref, a useLayoutEffect and a useEffect. Put what happens in order.
1.The browser paints the screen
2.React changes the DOM
3.React runs useEffect
4.React calls the component and compares the result
5.React attaches refs and runs useLayoutEffect
React189 / 204
Module 19 · Under the hood
useLayoutEffect: measure before the user sees
A tooltip must sit above its target, unless there isn’t room. You only know after the DOM exists, and you must move it before it is painted or it visibly jumps. That is the one job of useLayoutEffect. It blocks painting, so use it only for measuring.
The state update inside it is applied before paint: the user never sees the wrong position.
React190 / 204
Module 19 · Under the hood
Hydration: taking over HTML drawn on the server
Frameworks often render React to HTML on the server, so the page shows before any JavaScript runs. In the browser, hydrateRoot then hydrates it: React renders the same tree, matches it to the existing DOM instead of creating it, and attaches handlers. Hydration expects the first browser render to produce exactly the server’s HTML.
Find the bugInteractive · disabled in the PDF
This causes a hydration mismatch warning. Which line?
TSX
React191 / 204
Module 19 · Under the hood
useSyncExternalStore: reading stores React doesn’t own
Some data lives outside React: a state library, the browser’s online status, the window’s width. During a concurrent render, reading such a value directly can give different parts of the screen different answers, called tearing. useSyncExternalStore reads it safely and renders when it changes.
TSX
1functionsubscribe(onChange:()=>void){2 window.addEventListener('online', onChange)3 window.addEventListener('offline', onChange)4return()=>{5 window.removeEventListener('online', onChange)6 window.removeEventListener('offline', onChange)7}8}910exportfunctionuseOnline(){11returnuseSyncExternalStore(12 subscribe,13()=> navigator.onLine,// read in the browser14()=>true// read on the server15)16}
State libraries such as Zustand, Redux and Nano Stores use this Hook inside their own.
React192 / 204
Module 19 · Under the hood
Recap
Render phase: pure, interruptible, may run more than once. Commit: applies DOM changes at once.
Fibers hold each component’s props and Hook slots, in call order.
Reconciliation: different type replaces; same type updates; keys match lists.
Updates have priorities; transitions can be set aside and restarted.
useLayoutEffect runs before paint, for measuring; useEffect after.
Hydration needs the first browser render to match the server HTML.
useSyncExternalStore reads outside stores without tearing.
FlashcardsInteractive · disabled in the PDF
Fiber
React’s internal object for one component instance: type, props, state, Hooks.
Reconciliation
Comparing the new element tree with the current fibers to find changes.
Lane
React’s name for an update’s priority.
Hydration
React attaching to HTML the server already drew, without recreating it.
Tearing
Different parts of one screen showing different values of the same data.
useLayoutEffect
An effect that runs after DOM changes but before paint.
Module 20: Mastery: building and reviewing a large app
React193 / 204
Module 20 · Mastery: building and reviewing a large app
Organise by feature, not by kind of file
Small apps can keep every component in one folder. Large ones stay readable when code is grouped by feature: everything about to-dos together, everything about recipes together, and a small shared kit of generic pieces. A feature exposes a few things from an index.ts; the rest is private to it.
src/
app/# the shell: providers, routes, layout
features/
todos/
TodoList.tsx
TodoItem.tsx
todosReducer.ts
useTodos.ts
index.ts# what other features may import
recipes/
RecipeSearch.tsx
useRecipes.ts
api.ts
index.ts
ui/# generic: Button, Dialog, TextField
lib/# plain helpers, no React
Tip. A good test of a structure: could you delete a feature’s folder and fix the app by removing a few imports? If the answer involves many other folders, the features are tangled.
React194 / 204
Module 20 · Mastery: building and reviewing a large app
Each kind of state has a natural home
Kind
Example
Home
Local UI
Is this row being edited
useState in that component
Shared UI
The chosen filter
Lifted state, or a reducer plus context
Server data
Recipes from the API
A data library or the framework’s loaders: a cache, not state
In the address
Search text, page number
The URL, so it can be shared and survives reloads
Form drafts
Text being typed
The form: uncontrolled fields, or local state
Outside React
Online status, a shared store
useSyncExternalStore, or a store library
The most common large-app mistake. Copying server data into global state, then writing code to keep the copy fresh. Treat server data as a cache owned by one library, and derive everything else from it.
React195 / 204
Module 20 · Mastery: building and reviewing a large app
What to look for when reviewing React code
Purity: does anything in render change outside values, read refs, or depend on time or randomness?
State: is it minimal, in the lowest common parent, and never mutated?
Effects: does each one sync with an outside system, list honest dependencies, and clean up?
Keys: stable ids in lists; used deliberately to reset.
Async: loading, error and empty states; races guarded; errors caught by a boundary.
Access: real elements, names on every control, focus handled, changes announced.
Tests: do they use the screen the way a person does (roles, labels), not internal details?
Note. Testing is its own subject. The usual pair is a fast test runner such as Vitest with React Testing Library for components, plus a browser tool such as Playwright for whole journeys.
React196 / 204
Module 20 · Mastery: building and reviewing a large app
Challenge: the update queue
Predict the outputInteractive · disabled in the PDF
n is 0. After one click, what does the next render show?
TSX
1functionhandleClick(){2setN(n +5)3setN((x)=> x +1)4setN(42)5setN((x)=> x *2)6}
n is 0. After one click, what does the next render show?
Predict the outputInteractive · disabled in the PDF
The button is clicked once, then twice more within three seconds. What does the alert say the first time it shows?
A user has typed into NoteEditor for to-do 1, then selects to-do 2. In which versions is the draft cleared? Pick all.
React199 / 204
Module 20 · Mastery: building and reviewing a large app
Challenge: undo
Because every update makes a new array and leaves the old one alone, the old lists are still there to go back to. Undo is just keeping them.
Code challengeInteractive · disabled in the PDF
Add Undo: keep a history of past lists in state, push the current list before each delete, and make Undo restore the last one, one step at a time. Undo is disabled when there is nothing to undo.
TSX
1import{ useState }from'react'23const start =['Buy milk','Walk the dog','Water the plants']45exportdefaultfunctionTodos(){6const[todos, setTodos]=useState(start)7// TODO: a history of past lists8functionremove(text:string){9setTodos(todos.filter((t)=> t !== text))10}11functionundo(){12// TODO: go back one step13}14const rows = todos.map((t)=>(15<likey={t}>16{t}{' '}17<buttonaria-label={'Delete '+ t}onClick={()=>remove(t)}>18 ×19</button>20</li>21))22return(23<div>24<buttononClick={undo}>Undo</button>25<ul>{rows}</ul>26</div>27)28}
React200 / 204
Module 20 · Mastery: building and reviewing a large app
Challenge: write a debounce Hook
Fill the blanksInteractive · disabled in the PDF
Complete a Hook that returns value only after it has stopped changing for ms milliseconds.
Module 20 · Mastery: building and reviewing a large app
Challenge: a search that waits for you
Now write the debounce Hook for real and use it. The search box updates on every key, but the (pretend) server is asked only once you stop typing for 300 ms.
Code challengeInteractive · disabled in the PDF
Write useDebounced(value, ms): it returns value once it has stayed the same for ms milliseconds. Then typing Pea quickly calls search once, with Pea, and lists Pea soup.
TSX
1import{ useEffect, useState }from'react'23const all =['Pasta bake','Pea soup','Lemon cake']4constsearchRecipes=(q:string)=>5newPromise<string[]>((resolve)=>6setTimeout(()=>resolve(all.filter((r)=> r.includes(q))),50)7)89functionuseDebounced<T>(value:T, ms:number):T{10// TODO: hold back value until it stops changing for ms11return value12}1314exportdefaultfunctionRecipeSearch({ search = searchRecipes }){15const[query, setQuery]=useState('')16const settled =useDebounced(query,300)17const[found, setFound]=useState<string[]>([])18useEffect(()=>{19if(settled)search(settled).then(setFound)20},[settled, search])21return(22<div>23<label>24 Search recipes{' '}25<input26value={query}27onChange={(e)=>setQuery(e.target.value)}28/>29</label>30<ul>{found.map((r)=><likey={r}>{r}</li>)}</ul>31</div>32)33}
React202 / 204
Module 20 · Mastery: building and reviewing a large app
Challenge: async screens
QuizInteractive · disabled in the PDF
A recipe page reads use(recipePromise) from a cache, inside <Suspense>. Clicking “next recipe” calls setId inside startTransition. What does the user see while it loads?
A recipe page reads use(recipePromise) from a cache, inside <Suspense>. Clicking “next recipe” calls setId inside startTransition. What does the user see while it loads?
Put in orderInteractive · disabled in the PDF
You are adding a “favourite” button to each recipe that saves to the server. Order a solid implementation.
1.Wrap the button in a form whose action calls it
2.Write a Server Function or API call that saves the favourite
3.Catch a failed save in the action, report it with role="alert", and let the optimistic state roll back
4.At the start of that action, show the change at once with useOptimistic
React203 / 204
Module 20 · Mastery: building and reviewing a large app
Challenge: accessible tabs
Two buttons that swap a panel look like tabs, but a screen reader hears two unrelated buttons and two blocks of text. ARIA’s tab roles say what they are: a tablist holding tabs, one aria-selected, and a tabpanel named after its tab.
Code challengeInteractive · disabled in the PDF
Put the buttons in a role="tablist", give each role="tab", an id and aria-selected, and show only the current tab’s body in one role="tabpanel" with aria-labelledby set to its tab’s id.
TSX
1import{ useState }from'react'23const tabs =[4{id:'food',label:'Ingredients',body:'Flour, eggs, milk'},5{id:'method',label:'Method',body:'Whisk, rest, fry'}6]78exportdefaultfunctionRecipeTabs(){9const[current, setCurrent]=useState(tabs[0].id)10// TODO: the tab roles, and one panel for the current tab11return(12<div>13{tabs.map((tab)=>(14<buttonkey={tab.id}onClick={()=>setCurrent(tab.id)}>15{tab.label}16</button>17))}18{tabs.map((tab)=><pkey={tab.id}>{tab.body}</p>)}19</div>20)21}
React204 / 204
Module 20 · Mastery: building and reviewing a large app
Recap
Group large apps by feature, each with a small public index.ts.
Give each kind of state its home: local, lifted, cache, URL, form, external store.
Review for purity, minimal state, honest effects, keys, async states, access and tests.
Plain values replace in the queue; updaters build on it. Closures keep their render’s snapshot.
New objects in deps or props cause loops and wasted renders.
Type, position and key decide whether state is kept.
Transitions keep content on screen while the next one loads.
FlashcardsInteractive · disabled in the PDF
Feature folder
All the code for one feature together, with a small public surface.
Where does server data belong?
In a cache managed by a data library or framework, not copied into state.
Closure
A function remembering the variables of the place it was made, like one render’s state.
Why can an effect loop forever?
It sets state and depends on something that is new every render.
What keeps a component’s state?
Same type, same position, same key.
Pending UI in React 19
isPending from useTransition or useActionState, or useFormStatus.
Answer key
1. How a web page works · CSS says how things look · QuizYou want the to-do list’s text to be bigger. Which language do you change?B. CSSHow things look is CSS’s job: a rule like li { font-size: 1.2rem }. HTML would only change what is there, and JavaScript is for behaviour, like reacting to a click.
1. How a web page works · Check: what happens on a click · Put in orderSomeone clicks “Add a to-do” on the page above. Put what happens in order.1. The browser notices the click on the button 2. It runs the function you gave to addEventListener 3. The function creates an li and adds it to the ul 4. The DOM has changed, so the browser draws the new itemThe browser owns the events and the drawing. Your code only sits in the middle: it reacts to an event by changing the DOM, and the screen follows the DOM.
1. How a web page works · Check: what happens on a click · QuizAfter the click, does index.html on the server contain the new to-do?B. No, only the DOM in this browser tab changedJavaScript changes the DOM, the copy in memory. The file is untouched, so reloading the page brings back the original list. Saving data is a separate job you will meet later.
2. Setting up your first project · Check: the steps to a running project · Put in orderPut the steps in order, from nothing to a page in the browser.1. Install Node.js (it brings npm) 2. Run npm create vite@latest to make the project 3. Move into the project folder with cd 4. Run npm install to download the packages 5. Run npm run dev and open the address it printsNode.js comes first because every other step runs on it. npm install must come before npm run dev: the dev server needs React and Vite downloaded into node_modules to start.
2. Setting up your first project · Save a file and the page updates · QuizYou close the terminal window that runs npm run dev. What happens to http://localhost:5173?B. It stops working: the dev server ran in that terminallocalhost means “this computer”. The dev server is a program running in your terminal, and only you can reach it. Close it and the address stops answering. Publishing a site for others is a separate step, npm run build, and a host.
2. Setting up your first project · Errors are messages, so read them · Find the bugThis file won’t compile. Which line is wrong?Line 3: }A function’s body opens with { on line 1, so it must close with }. A ) closes a parenthesis that was never opened. Brackets come in pairs, and most early errors are a missing or mismatched one.
3. JSX from scratch · Check: what ends up on the page · QuizWhat text does this show?B. 2 of 5 done, total - done leftOnly what is inside braces is JavaScript. total - done is outside them, so it is plain text and shows as it is written. To compute it, write {total - done}.
3. JSX from scratch · Check: what ends up on the page · Fill the blanksShow the to-do’s text inside the list item.1: {text}Braces around text tell JSX to use the variable’s value. Without them you would see the word “text”.
3. JSX from scratch · Every tag must be closed · Find the bugThis JSX won’t compile. Which line is the problem?Line 4: <input id="new-todo" />An input has no content, so in JSX it must close itself with />. Left open, the translator thinks the button and </form> are inside the input and stops at the mismatch.
3. JSX from scratch · Your first component returns JSX · QuizYou rename it function app() and write <app />. What happens?B. React makes an unknown HTML tag called app instead of calling your functionJSX decides by the first letter. Lowercase means a built-in HTML tag, passed as the text "app". Capitalised means a variable, your function. That is why component names must start with a capital.
3. JSX from scratch · Try it: a recipe card · Code challengeFinish RecipeCard: above the list, add a heading showing title, and a paragraph that reads Serves 4, using servings inside braces.One solution:
Solution
1exportdefaultfunctionRecipeCard(){2const title ='Pancakes'3const servings =44return(5<article>6<h2>{title}</h2>7<p>Serves {servings}</p>8<ul>9<li>Flour</li>10<li>Eggs</li>11<li>Milk</li>12</ul>13</article>14)15}
Braces put a JavaScript value into the markup: {title} becomes the text Pancakes. Text and braces can sit side by side, as in Serves {servings}, and React joins them into one line. The usual slip is to leave the braces out: <h2>title</h2> shows the word “title”, not the value. Any heading level works, since the test asks for the heading role.
4. Components · One component per file, shared with import and export · Fill the blanksTodoList.tsx has export function TodoList(). Import it in App.tsx.1: { TodoList }, 2: ./TodoListA named export is imported with braces and its exact name. The path starts with ./ because the file sits next to App.tsx. Without the ./, the tools would look for a package called TodoList.
4. Components · The same component can appear many times · QuizWhen React renders Page above, how many times does it call Divider?B. Twice, once per <Divider />Every tag in the JSX is its own instance, and React calls the component for each one. That is what lets each copy have different data once you add props.
4. Components · Try it: five stars · Code challengeFinish Star so it returns <span>★</span>, and make Rating show five <Star />s inside its paragraph.One solution:
Each <Star /> is its own use of the component: React calls Star five times and puts each result in place. A component that returns null draws nothing, which is why the starter shows no stars at all. Writing <span>★</span> five times would pass too, but then a change to the star means five edits; the component makes it one.
4. Components · Components must be pure · Find the bugThis component gives a different result each time React calls it. Which line breaks purity?Line 4Line 4 changes a variable that lives outside the component. React may call a component more often than you expect, so the numbers would jump around. Information a component needs should come in as props (next module), not from outside variables it changes.
4. Components · Strict Mode renders twice to catch impure code · QuizIn development, a console.log inside a component prints twice per render. Is something broken?B. No: Strict Mode calls components twice on purpose to find impuritiesThis is Strict Mode doing its job. React’s developer tools dim the second log so you can tell them apart. It only happens in development, so it costs nothing in the published app.
4. Components · Check: from a design to components · Put in orderYou are given a picture of the to-do app. Put the steps of turning it into components in order.1. Draw a box around each part of the picture and name it 2. Arrange the boxes into a tree: which box sits inside which 3. Write each box as a component that returns static JSX 4. Use the children inside their parents, starting from AppStart from the picture, not the code. Naming boxes and finding the tree first means each component has one clear job before you write it. Static JSX first, data later, is the approach the React docs call “thinking in React”.
5. Props: passing data down · Check: write a component with props · Fill the blanksComplete the component so <Greeting name="Sam" /> shows “Hello, Sam”.1: name, 2: {name}Destructure name out of the props object, then put it inside braces in the JSX. Without the braces you would see the word “name”.
5. Props: passing data down · Check: write a component with props · QuizWhat does React pass as the first argument to TodoItem for <TodoItem text="Buy milk" done={false} />?B. An object: { text: "Buy milk", done: false }A component always gets exactly one argument: an object holding every prop. Destructuring is just a short way of reading its fields.
5. Props: passing data down · Try it: a greeting with a default · Code challengeMake Greeting show Hello, Ada! for <Greeting name="Ada" />, and Hello, friend! when no name is given. Use a default value in the destructuring.One solution:
Solution
1typeGreetingProps={2 name?:string3}45exportdefaultfunctionGreeting({ name ='friend'}:GreetingProps){6return<p>Hello, {name}!</p>7}
{ name = 'friend' } reads the name prop and uses 'friend' when it is undefined, which is what a missing prop is. Writing name || 'friend' in the JSX works too, but it also replaces an empty string; the default only fills in a missing prop. Each <Greeting /> gets its own props, so the two in the last test greet different people.
5. Props: passing data down · Props are read-only · Find the bugThis component breaks a rule of props. Which line?Line 3: const text = todo.text || 'Untitled'Line 3 changes the parent’s object. The parent’s data now differs from what it thinks it has, and the change happened during rendering, so it isn’t pure either. Work out a new value in a local variable instead, and show {text}.
5. Props: passing data down · Data flows one way: down · QuizA TodoItem needs to tell the app it was ticked. With what you know about props, how could it?B. The parent passes it a function as a prop, and the child calls itData still flows down: the function is just another prop. When the child calls it, code in the parent runs, and the parent decides what changes. You will build exactly this in the events and state modules.
6. Lists and conditions · Keys tell React which row is which · QuizWhich is the best key for a to-do row?B. todo.id, made once when the to-do was createdA key must be unique and stable. Math.random() changes every render, so React rebuilds every row each time. Text can repeat (two “Buy milk”s). The index shifts when items move. An id made once with the item is both.
6. Lists and conditions · Filter first, then map · Fill the blanksShow only the finished to-dos, each with its id as key.1: filter, 2: todo.idfilter keeps the items whose function returns true, here the done ones. Then map turns each into a li, keyed by the to-do’s own id.
6. Lists and conditions · Inside JSX: the ternary and && · QuizWith no to-dos, what does this show?B. A 0a && b gives back a when a is falsy. todos.length is the number 0, and React does draw numbers, so a lone 0 appears. Compare explicitly: todos.length > 0 && …, which gives false, and false draws nothing.
6. Lists and conditions · A component can return null to show nothing · Find the bugThe list renders nothing at all. Which line is the bug?Line 5: return <li key={todo.id}>{todo.text}</li>With { } after the arrow, the function has a body and needs an explicit return. Without it, each call returns undefined, which draws nothing. Either add return, or use ( ) instead of { }, which returns the JSX directly.
6. Lists and conditions · Try it: a list with an empty state · Code challengeMake TodoList show only the to-dos that are not done, one li each. When none are left, show a paragraph Nothing to do. Enjoy. instead of the list.One solution:
Solution
1typeTodo={ id:number; text:string; done:boolean}23const sample:Todo[]=[4{id:1,text:'Buy milk',done:false},5{id:2,text:'Walk the dog',done:true}6]78exportdefaultfunctionTodoList({ todos = sample }){9const open = todos.filter((todo)=>!todo.done)10if(open.length ===0){11return<p>Nothing to do. Enjoy.</p>12}13return(14<ul>15{open.map((todo)=>(16<likey={todo.id}>{todo.text}</li>17))}18</ul>19)20}
Filter first, then decide: the empty check must look at the filtered list, or a list of finished to-dos shows an empty ul instead of the message (the last test). An early return keeps the two screens apart. The key stays the to-do’s id, never its position, so rows keep their identity when the filter changes.
7. Events and state · Pass a function to onClick · Find the bugThe alert pops up as soon as the page loads, not on click. Which line?Line 5: return <button onClick={handleClick}>Clear done</button>handleClick()calls the function during rendering and passes its result (nothing) to onClick. handleClick without brackets passes the function itself, for React to call later. To pass an argument, wrap it: onClick={() => remove(id)}.
7. Events and state · A plain variable can’t make the page change · QuizYou click the button five times. What does it say?B. Clicked 0 timesTwo problems. Changing a local variable doesn’t tell React to render again, so the screen never updates. And even if it did render, let count = 0 would run again and start from zero. A component needs a way to remember a value and to ask for a new render. That is state.
7. Events and state · Try it: a counter · Code challengeMake the first button count its clicks (Clicked 1 times, Clicked 2 times…), and make Reset set the count back to 0.One solution:
Each onClick gets a function for React to call later: () => setCount(count + 1). The setter stores the new number and asks for a render, and the render shows it. Writing onClick={setCount(count + 1)} instead calls the setter while rendering, which renders again, and again: React stops it with a “too many re-renders” error.
7. Events and state · Trigger, render, commit · Put in orderYou click the counter. Put what happens in order.1. The onClick handler calls setCount(1) 2. React stores 1 and schedules a render of Counter 3. React calls Counter; useState now returns 1 4. React compares the new JSX with the old and finds the text changed 5. React updates that text in the DOM, and the browser draws itThe setter doesn’t change the screen itself: it records the new value and asks for a render. Rendering is only calling your function. The DOM is touched in the commit, and only where the result differs.
7. Events and state · Each copy of a component has its own state · QuizA page shows <Counter /> twice. You click the first one three times. What do they show?B. 3 and 0Each <Counter /> is its own instance with its own memory. Only the clicked one’s setter ran, so only it changed.
8. Updating state the right way · State is a snapshot for one render · Quizcount is 0. You click once. What does it show after?B. 1In this render count is 0 on all three lines, so each line asks for 0 + 1. The next render gets 1. The setter doesn’t update the variable you are holding.
8. Updating state the right way · To build on the latest value, pass a function · Fill the blanksToggle open from whatever it currently is, safely.1: open, 2: !openAn updater gets the latest value and returns the next. Name the parameter anything; by habit it is the state’s name or its first letter. ! turns true into false and back.
8. Updating state the right way · Change one item with map · Find the bugTicking a box sometimes doesn’t update the screen. Which line is the bug?Line 3: if (t.id === id) return { ...t, done: !t.done }The array is new, but line 3 changed the old to-do object in place. Any component that compares that object (a memoised row, or the last render’s snapshot) sees “same object” and may not update. Copy the item you change, too.
8. Updating state the right way · Try it: tick and delete · Code challengeWrite toggle and remove with new arrays: ticking a box marks that to-do done (so 1 left), and its Delete button takes it off the list.One solution:
Solution
1import{ useState }from'react'23const start =[4{id:1,text:'Buy milk',done:false},5{id:2,text:'Walk the dog',done:false}6]78exportdefaultfunctionTodos(){9const[todos, setTodos]=useState(start)10functiontoggle(id:number){11setTodos((todos)=>12 todos.map((t)=>(t.id === id ?{...t,done:!t.done }: t))13)14}15functionremove(id:number){16setTodos((todos)=> todos.filter((t)=> t.id !== id))17}18const rows = todos.map((t)=>(19<likey={t.id}>20<label>21<inputtype="checkbox"checked={t.done}22onChange={()=>toggle(t.id)}/>23{t.text}24</label>25<buttononClick={()=>remove(t.id)}>Delete</button>26</li>27))28const left = todos.filter((t)=>!t.done).length29return<div><p>{left} left</p><ul>{rows}</ul></div>30}
map makes a new array, and { ...t, done: !t.done } a new object for the one that changed, while the others are passed along as they are. filter makes a new array without the deleted one. Flipping t.done in place and calling setTodos(todos) hands React the same array, so it sees nothing new and skips the render; it also changes start, which the next test begins from.
8. Updating state the right way · Don’t store what you can work out · QuizIn a to-do app with a filter, which of these should be state? Pick all.A. The list of to-dos; B. The chosen filter (all, open, done)The to-dos and the filter change over time and can’t be worked out from anything else, so they are state. The filtered list and the count can be computed from those two on every render, so storing them would only create copies that can disagree.
9. Forms and inputs · Check: a frozen input · Find the bugYou can’t type in this input: every key press is ignored. Which line is missing a piece?Line 4: <input value={query} onChange={(e) => setQuery(e.target.value)} />With value but no onChange, React keeps putting query (empty) back after every key press. React warns about exactly this in the console. Either add onChange, or use defaultValue if you don’t need to control it.
9. Forms and inputs · Try it: a form that adds to-dos · Code challengeFinish handleSubmit: add the trimmed draft to todos (as { id, text }), then empty the field. Ignore a draft that is only spaces.One solution:
Solution
1import{ useState,type SubmitEvent }from'react'23typeTodo={ id:number; text:string}45exportdefaultfunctionTodoForm(){6const[todos, setTodos]=useState<Todo[]>([])7const[draft, setDraft]=useState('')89functionhandleSubmit(event:SubmitEvent){10 event.preventDefault()11const text = draft.trim()12if(text ==='')return13setTodos([...todos,{id: todos.length +1, text }])14setDraft('')15}1617return(18<formonSubmit={handleSubmit}>19<label>20 New to-do{' '}21<input22value={draft}23onChange={(e)=>setDraft(e.target.value)}24/>25</label>26<button>Add</button>27<ul>28{todos.map((t)=><likey={t.id}>{t.text}</li>)}29</ul>30</form>31)32}
The form’s onSubmit runs for a click on its button and for Enter in the field, and preventDefault() stops the page reloading. Trim first, then check, so “ ” counts as empty. Clearing the field is just setDraft(''): the input is controlled, so it shows whatever the state says. The id here is the list’s length plus one, which is enough while nothing is deleted; a real app gets ids from its data.
9. Forms and inputs · Checkboxes, selects and text areas · Fill the blanksControl the “show finished” checkbox.1: checked, 2: checkedFor checkboxes the state is checked, a true or false, not value. e.target.value on a checkbox is the text "on", whatever its state, which is a classic mistake.
9. Forms and inputs · Editing a to-do in place · QuizElsewhere the to-do is renamed while this row isn’t editing. Then you double-click it. What does the input show?B. The old nameuseState(todo.text) only uses its argument on the first render. Later changes to the prop don’t reach draft. Fix it by setting the draft when editing starts: setDraft(todo.text); setEditing(true). The sharing-state module shows another fix, with a key.
10. Sharing state between components · The child becomes controlled by its parent · QuizA count of open to-dos now sits in the Header, also a child of App. Where should the to-dos live?C. In AppApp is the closest parent of everything that reads the to-dos. Two copies would drift apart. Put it in one place and pass down what each child needs: a single source of truth.
10. Sharing state between components · Try it: lift the search up · Code challengeMove query up into Bookshop. Make SearchBox controlled, with value and onChange props, and pass the query to BookList, so typing in Search narrows the list.One solution:
State is private, so the list can only see the query if a parent of both owns it. Bookshop keeps it and passes it down twice: as value to show in the box, and as query to filter by. Changes travel up through onChange. Keeping a second useState in SearchBox as well is the usual slip: then there are two queries that drift apart.
10. Sharing state between components · Check: lift some state · Put in orderThe “show finished” checkbox lives in Settings, and now TodoList needs it too. Put the refactor in order.1. Find the closest parent of Settings and TodoList 2. Move the useState from Settings into that parent 3. Pass the value to TodoList and Settings as props 4. Pass a handler to Settings and call it from the checkboxFind the home first, then move the state, then wire it down. The handler is the piece people forget: without it, the checkbox shows the value but can no longer change it.
10. Sharing state between components · State belongs to a position in the tree · QuizA to-do row is being edited. You tick “show finished”, so the list re-renders with the same row in the same place. Is the half-typed draft kept?A. YesSame component, same position, same key: React keeps its state. If ticking the box had removed the row from the list, its state would be gone, and showing it again would start fresh.
11. Effects, and when not to use them · Check: when does it run · QuizThe component renders with filter going all, all, done. How many times does the effect run?B. TwiceIt runs after the first render (all), skips the second because filter is the same (React compares with Object.is), and runs after the third (done). In development, Strict Mode also runs it an extra time at first: see cleanups.
11. Effects, and when not to use them · Try it: keep the tab’s title in step · Code challengeAdd an effect that sets document.title to Inbox (3) while 3 are unread, and to just Inbox when none are. It should run again whenever unread changes.One solution:
The effect runs after React has updated the screen, and the dependency array [unread] makes it run again only when the count changed. Leaving the array out works here but runs after every render; [] would set the title once and never again, so it would stay at 3. Nothing needs undoing, so this effect has no cleanup.
11. Effects, and when not to use them · Check: a leaking listener · Find the bugAfter opening and closing this panel a few times, every key press runs the handler many times. Which line is the bug?Line 6: window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)Each time the effect runs it adds a listener and never removes one, so they pile up. Return () => window.removeEventListener('keydown', onKey) right after adding it. Every “add” in an effect needs its “remove”.
11. Effects, and when not to use them · You don’t need an effect to respond to an event · QuizWhich of these need an effect? Pick all.B. Keeping a connection to a live chat open while the chat is on screen; E. Starting a video player library on a DOM node when it appearsThe chat connection and the video library are outside systems tied to the component being on screen. Saving is an action: use the handler. The count is computed during render. Resetting a form is a job for a key.
12. Refs and the DOM · State or ref? · QuizWhat does the button show after three clicks?B. 0count.current really is 3 after three clicks, but nothing asked React to render, so the screen still shows 0. Anything shown on screen belongs in state. This is also why you shouldn’t read refs during render.
12. Refs and the DOM · Pass a ref to an element to reach its DOM node · Fill the blanksFocus the search box when the button is clicked.1: useRef, 2: ref, 3: currentMake the ref with useRef, attach it with the ref prop, and read the node from .current in the handler, after React has filled it in.
12. Refs and the DOM · Try it: put the cursor back · Code challengeMake a ref with useRef, attach it to the input, and in clearSearch call focus() on the node, so after Clear the Search recipes field has the cursor.One solution:
useRef<HTMLInputElement>(null) makes an empty box; ref={input} asks React to put the input’s DOM node in input.current after the commit. The handler runs later, when the node is there, so input.current?.focus() works. Reading input.current during rendering is the mistake to avoid: on the first render it is still null.
12. Refs and the DOM · Reach for the DOM only for what React can’t do · Find the bugThis works once, then strange things happen when the list changes. Which line is the problem?Line 5: setTodos((todos) => todos.filter((t) => !t.done))It removes DOM nodes React thinks it owns. The to-dos are still in state, so React’s picture of the page and the real page now disagree. Change the data and let React update the DOM.
12. Refs and the DOM · Expose a small API with useImperativeHandle · QuizA parent wants a modal to be open or closed. Prop or ref?B. An open prop, with an onClose handlerWhether it is open is data, and data flows through props, where React can see it. A ref method would hide the state inside the child, so the parent couldn’t know or render from it. Save refs for true one-off actions like focus.
13. Reducers and context · useReducer: dispatch actions instead of setting state · QuizWhat does todosReducer return here?A. A new array with only the “Dog” to-doThe deleted branch returns todos.filter(…), a new array without id 1. Reducers never change the array they are given. Because a reducer is a plain function, you can test it exactly like this, without rendering anything.
13. Reducers and context · Try it: a cart reducer · Code challengeWrite cartReducer. added adds a line { name, qty: 1 }, or adds one to the qty of the line with that name; cleared empties the cart. Return new arrays and objects.One solution:
Solution
1import{ useReducer }from'react'23typeLine={ name:string; qty:number}4typeAction={ type:'added'; name:string}|{ type:'cleared'}56functioncartReducer(cart:Line[], action:Action):Line[]{7switch(action.type){8case'added':{9const line = cart.find((l)=> l.name === action.name)10if(!line)return[...cart,{name: action.name,qty:1}]11return cart.map((l)=>12 l === line ?{...l,qty: l.qty +1}: l13)14}15case'cleared':16return[]17}18}1920exportdefaultfunctionShop(){21const[cart, dispatch]=useReducer(cartReducer,[])22constadd=(name:string)=>dispatch({type:'added', name })23constclear=()=>dispatch({type:'cleared'})24return(25<div>26<buttononClick={()=>add('Apple')}>Add an apple</button>27<buttononClick={()=>add('Pear')}>Add a pear</button>28<buttononClick={clear}>Clear</button>29<ul>30{cart.map((l)=><likey={l.name}>{l.name} × {l.qty}</li>)}31</ul>32</div>33)34}
A reducer is a plain function from the cart and an action to the next cart, so the last test can call it without drawing anything. find tells a new item from one already there; map with a spread makes a new line for the one that changes. line.qty++ would pass the screen tests but fail the last one: changing the old state in place is what reducers must never do.
13. Reducers and context · useState or useReducer? · Fill the blanksAdd a “cleared” action that removes every finished to-do.1: filter, 2: !t.doneKeep the to-dos that are not done. filter returns a new array, so state isn’t mutated. Add | { type: 'cleared' } to the Action type too, and TypeScript will check every dispatch.
13. Reducers and context · Try it: read a context · Code challengeIn Greeting, read LangContext with useContext, and show Bonjour when it is fr, Hello otherwise. Don’t pass any new props.One solution:
useContext(LangContext) finds the nearest <LangContext value=…> above, however deep, so Card in the middle needs no props. When the provided value changes, every component reading it renders again. Without a provider above, useContext returns the default given to createContext, here en.
13. Reducers and context · Context is not free: use it for wide, slow-changing data · QuizWhich are good fits for context? Pick all.A. The colour theme; B. The signed-in userTheme and user are read in many places and change rarely. A search box used by one sibling is a case for lifting state. One level down is just a prop. Context is for data many distant components need.
14. Custom Hooks and fetching data · Use it like a built-in Hook · QuizTwo different components both call useStoredState('todos', []). Do they share one list in memory?B. No: each call has its own state; Hooks share logic, not stateA custom Hook is just a function. Each component that calls it gets its own useState inside. Both read the same saved text at first, but after that they drift apart. To share data, lift the state or use context.
14. Custom Hooks and fetching data · Rules for your own Hooks · Find the bugThis throws an error about Hooks when you click. Which line?Line 3: const [, setSaved] = useStoredState('backup', todos)A Hook called inside a handler runs only sometimes, so React can’t match it to the same slot every render. Call it at the top of the component and use the setter in the handler.
14. Custom Hooks and fetching data · Ignore answers that arrive too late · Find the bugWhich line should change so that a late answer to an old query is ignored?Line 4: if (!ignore) setRecipes(found)The cleanup runs when query changes, setting this run’s ignore to true. But the result is stored without checking it. Guard it: if (!ignore) setRecipes(found). Each effect run has its own ignore, so only the newest one is listened to.
14. Custom Hooks and fetching data · Wrap it in a Hook · QuizTyping “lasagne” fires seven requests, one per letter. What is the problem, and a common fix?B. Wasted requests; wait until typing pauses before searching (a debounce)The guard keeps the screen correct, but the server still does seven searches. A debounce waits, say, 300 ms after the last key press before using the text. Later you will meet useDeferredValue, which tackles the same feel from React’s side.
14. Custom Hooks and fetching data · Try it: a Hook that loads recipes · Code challengeFinish useRecipes: keep recipes and error in state, and in an effect call searchRecipes(query), storing the result or the error. Ignore answers that arrive after the query changed.One solution:
Fetching keeps the screen in step with query, so it belongs in an effect with [query] as its dependency. .then(onDone, onFail) stores one of the two. The ignore flag is the race guard: when query changes, the cleanup marks the old request stale, and its answer is dropped. Calling searchRecipes straight in the Hook’s body, outside an effect, would start a new search on every render, and storing its answer would render again: a loop.
15. Accessible components · Everything you can use needs a name · Find the bugA screen reader announces one of these as just “checkbox”. Which line needs fixing?Line 2: <input type="checkbox" aria-label={todo.text} checked={todo.done}The text sits in a span that isn’t connected to the checkbox. Give it a name with aria-label={todo.text}, or better, wrap both in a <label> so clicking the text ticks the box too.
15. Accessible components · Try it: name every control · Code challengeWrap each checkbox and its text in a <label>, so the box is named after the to-do, and give the × button the name Delete plus the to-do’s text (Delete Buy milk).One solution:
Solution
1typeTodo={ id:number; text:string; done:boolean}23functionTodoRow({ todo }:{ todo:Todo}){4return(5<li>6<label>7<inputtype="checkbox"defaultChecked={todo.done}/>8<span>{todo.text}</span>9</label>10<buttontype="button"aria-label={'Delete '+ todo.text}>11 ×12</button>13</li>14)15}1617exportdefaultfunctionList(){18return(19<ul>20<TodoRowtodo={{id:1,text:'Buy milk',done:false}}/>21<TodoRowtodo={{id:2,text:'Walk the dog',done:true}}/>22</ul>23)24}
A label around an input names it with the label’s text, and clicking the text ticks the box too: a bigger target for everyone. A button showing only a symbol needs aria-label, which replaces its text as its name. “Delete” alone would pass a checker but leave a list of identical buttons; adding the to-do says which one.
15. Accessible components · ARIA describes what HTML can’t · QuizYou need a toggle between “all” and “done”. What is the best first choice?B. Two buttons with aria-pressedReal buttons are focusable and keyboard-operable already, and aria-pressed adds the one thing HTML lacks: on or off. The first option can work, but re-builds by hand what button gives for free, and is easy to get subtly wrong.
15. Accessible components · Try it: a disclosure that says if it’s open · Code challengeThe Ingredients button already shows and hides the list. Add aria-expanded so it tells a screen reader whether the list is open, and aria-controls pointing at the list’s id.One solution:
A sighted person sees the list appear; a screen-reader user hears “Ingredients, button, collapsed”, then “expanded”, only if the button says so. React writes aria-expanded={false} as the text "false", which is what the attribute needs. The state is the one you already have: no new state, just one more place it shows.
15. Accessible components · Check: testing accessibility · Put in orderA cheap, effective routine for checking a new screen. Put it in order, from quickest to most thorough.1. Let the linter flag issues as you write (eslint-plugin-jsx-a11y) 2. Run an automated checker such as axe or Lighthouse in the browser 3. Put the mouse away and use the screen with only the keyboard 4. Use it with a screen reader (VoiceOver, NVDA or TalkBack)Automated tools catch perhaps a third of problems, cheaply. The keyboard pass finds unreachable controls and lost focus. A screen reader finds missing names and silent updates. Each step finds things the last one can’t.
15. Accessible components · Check: testing accessibility · QuizAn automated checker reports zero issues. Is the screen accessible?B. Not necessarily: tools can’t judge if names make sense or focus goes somewhere usefulA tool can see that a button has a name, not that “btn-3” is a useless one. Whether the flow makes sense needs a person.
16. Performance · Why a component renders · QuizYou type in AddTodo’s input, whose draft is state in App. Which components render?B. App, AddTodo and TodoList (and every row)The state lives in App, so App renders, and its children render with it. TodoList gets the same todos, but React doesn’t check props unless asked to. Rendering is usually cheap; it matters when the list is long or rows are heavy.
16. Performance · Try it: stop the chart redrawing · Code challengeMake typing in Note leave the chart alone, so it stays at Chart drawn 1 times. Either move the note’s state down into a NoteField component, or wrap SlowChart in memo.One solution:
Solution
1import{ memo, useRef, useState }from'react'23const SlowChart =memo(functionSlowChart(){4// A render counter, for this lesson only: real code5// mustn't change a ref while rendering.6const draws =useRef(0)7 draws.current++8return<p>Chart drawn {draws.current} times</p>9})1011exportdefaultfunctionDashboard(){12const[note, setNote]=useState('')13return(14<div>15<label>16 Note{' '}17<inputvalue={note}onChange={(e)=>setNote(e.target.value)}/>18</label>19<SlowChart/>20</div>21)22}
Typing sets state in Dashboard, and a render of a parent renders all its children. memo makes React compare SlowChart’s props first; it has none, so they never differ and the last result is reused. Moving the note into its own NoteField fixes it without memo: then the state change renders only NoteField, and Dashboard never runs. Prefer that when it fits: it can’t be undone by a new object in the props.
16. Performance · New objects and functions defeat memo · Find the bugTodoItem is wrapped in memo, yet every row still renders when the draft changes. Which line is the cause?Line 3: const toggle = useCallback((id: number) => dispatch({ type: 'toggled', id }), [dispatch])toggle is a new function on each render, so every row sees a changed onToggle. Wrap it: useCallback((id) => dispatch(…), [dispatch]). dispatch never changes, so toggle stays the same function.
16. Performance · The React Compiler memoises for you · QuizWhy does the compiler need components to be pure and state immutable?B. Memoising is only safe if the same inputs always mean the same output, and a change always means a new objectReusing an old result is only correct if nothing hidden could have changed it. Mutation hides changes inside “the same” object, and impure renders depend on things outside the inputs. Every rule in this course is also what makes automatic memoisation possible.
16. Performance · Check: a slow screen · Put in orderTyping in the search box of the recipe browser feels laggy. Put a sound approach in order.1. Reproduce it in a production build 2. Record typing in the Profiler and find what renders on each key press 3. Move state down or restructure so less of the tree depends on it 4. If it is still slow, memoise the expensive part or defer it 5. Measure again to confirm it helpedMeasure in production, because development mode adds checks that distort timing (to record a production build in the Profiler, use React’s profiling build). Structure fixes are free and remove whole subtrees from the work; memoisation and deferring come after. Always confirm with a second measurement.
17. Suspense and transitions · Try it: a boundary in the right place · Code challengeWrap BookList, and only BookList, in <Suspense> with the fallback <p>Loading books…</p>, so the heading shows at once and the list replaces the fallback when it is ready.One solution:
Solution
1import{ Suspense, use }from'react'23constfetchBooks=()=>4newPromise<string[]>((resolve)=>5setTimeout(()=>resolve(['Dune','Emma','Beloved']),200)6)7const booksPromise =fetchBooks()89functionBookList(){10const books =use(booksPromise)11return<ul>{books.map((b)=><likey={b}>{b}</li>)}</ul>12}1314exportdefaultfunctionShop(){15return(16<main>17<h1>Bookshop</h1>18<Suspensefallback={<p>Loading books…</p>}>19<BookList/>20</Suspense>21</main>22)23}
When BookList suspends, React shows the fallback of the nearest Suspense above it, and everything outside that boundary stays. Put the boundary around the whole main and the heading disappears too; leave it out and nothing shows until the data is there. The last test waits 600 ms, not 200: once a fallback is shown, React keeps it at least 300 ms, so content doesn’t flash in and out.
17. Suspense and transitions · Where you put boundaries decides what the user sees · QuizIn the tree above, Reviews suspends but RecipeDetails is ready. What does the user see?B. The recipe details, with a reviews spinner belowOnly the nearest boundary above Reviews falls back. The outer one has everything it needs, so the details show. Nesting boundaries is how you let important content appear first.
17. Suspense and transitions · Check: which updates go in a transition · QuizWhich update must not be put in a transition?B. Setting a controlled input’s value as the user typesAn input’s value must update right away, or typed letters lag and can be lost. Keep the input’s state urgent; if something expensive depends on it, defer that part, as on the next slide.
17. Suspense and transitions · useDeferredValue lets a slow part lag behind · Fill the blanksLet the expensive chart lag behind the slider.1: useDeferredValue, 2: laggingThe slider uses size and stays smooth; the chart gets the deferred copy. For this to help, Chart must be wrapped in memo (or compiled by the React Compiler), so it can skip renders while lagging hasn’t changed.
18. Actions, use and the server · useActionState keeps the result of the last submit · QuizA function given to useActionState is written async (data: FormData) => …. What goes wrong?B. data is actually the previous result; the form data is the second argumentReact always calls it with (previousState, formData). Forgetting the first parameter is the most common mistake with this Hook, and TypeScript catches it if you type the parameters.
18. Actions, use and the server · useFormStatus reads the form a component is inside · Find the bugpending is always false here. Which line is the problem?Line 4It reads the status of a parent form, and this component renders the form rather than sitting inside it. Move the button into its own component, function Submit() { const { pending } = useFormStatus(); … }, and render <Submit /> inside the form.
18. Actions, use and the server · Try it: a like that lands at once · Code challengeUse useOptimistic(likes) so a click shows Like (11) at once, inside the Action, while the save runs. If the save fails, the button should settle back to Like (10), with Not saved shown.One solution:
useOptimistic(likes) returns likes itself until an Action sets a temporary value; while the Action runs, the button shows that instead. When the Action ends, the temporary value is dropped and the real state shows: 11 if the save worked and setLikes ran, 10 if it failed. No rollback code is needed. Calling setShown outside an Action does nothing useful, and React warns about it.
18. Actions, use and the server · Check: server or client · QuizIn a framework with Server Components, which parts of the recipe page must be Client Components? Pick all.B. A “save to favourites” button that shows a pending state; C. A servings slider that rescales ingredient amounts liveAnything with state, effects or event handlers runs in the browser: the button and the slider. Static content from the database is ideal for a Server Component: no code shipped, data read directly. Keep the client parts small, at the leaves of the tree.
19. Under the hood · Reconciliation: comparing the new tree with the old · QuizA to-do row is being edited. Then this condition flips. Is the draft kept?B. No: the type at that position changed from TodoItem to divAt the top position, React sees div where TodoItem was, so it destroys the old subtree, state included. Keep the structure the same and change props instead: <div className={highlighted ? 'glow' : ''}> around the row in both cases.
19. Under the hood · Check: the order of an update · Put in orderA state change updates a component that has a ref, a useLayoutEffect and a useEffect. Put what happens in order.1. React calls the component and compares the result 2. React changes the DOM 3. React attaches refs and runs useLayoutEffect 4. The browser paints the screen 5. React runs useEffectLayout effects run after the DOM changes but before the browser paints, so they can measure and adjust without a flicker. Normal effects run after paint, so they never delay what the user sees.
19. Under the hood · Hydration: taking over HTML drawn on the server · Find the bugThis causes a hydration mismatch warning. Which line?Line 2The server and the browser may be in different time zones, or render at different moments, so the text can differ. Render something stable first, then fill in the browser-only value in an effect, or show it only after mounting. The same applies to Math.random(), localStorage and window.
20. Mastery: building and reviewing a large app · Challenge: the update queue · Quizn is 0. After one click, what does the next render show?A. 84React processes the queue in order. n + 5 uses the snapshot: replace with 5. Then 5 + 1 = 6. Then replace with 42. Then 42 × 2 = 84. Plain values replace; updaters build on whatever came before them.
20. Mastery: building and reviewing a large app · Challenge: the update queue · QuizThe button is clicked once, then twice more within three seconds. What does the alert say the first time it shows?A. 0The timeout’s function belongs to the render where count was 0, and keeps that snapshot forever (it closes over it). Each later click’s alert shows its own render’s value: 1, then 2. To read the latest value later, keep it in a ref.
20. Mastery: building and reviewing a large app · Challenge: an endless loop · Find the bugThis component sends requests forever. Which line is the root cause?Line 3options is a new object every render, so the effect runs after every render. Each run sets new recipes, which renders, which makes a new options, and so on. Build the object inside the effect and depend on [query]. The race guard is correct but can’t stop the loop.
20. Mastery: building and reviewing a large app · Challenge: an endless loop · QuizA memo-wrapped TodoItem still renders on every key press in an unrelated input. Which could be the cause? Pick all.A. Its parent passes style={{ padding: 8 }}; B. It reads a context whose value is { todos, filter }, made fresh in each provider renderAn inline object is a new prop each render, so memo sees a change. A context whose value is a new object makes every reader render, and memo doesn’t stop context updates. A stable key and unchanged own state cause nothing.
20. Mastery: building and reviewing a large app · Challenge: kept or reset · QuizA user has typed into NoteEditor for to-do 1, then selects to-do 2. In which versions is the draft cleared? Pick all.B. B; D. DB changes the key, so React treats it as a new editor. In D the editor appears inside a list keyed by id, so each to-do has its own: to-do 1’s editor unmounts. A and C render the same type in the same place, so the state is kept, even though C looks like two components.
20. Mastery: building and reviewing a large app · Challenge: undo · Code challengeAdd Undo: keep a history of past lists in state, push the current list before each delete, and make Undo restore the last one, one step at a time. Undo is disabled when there is nothing to undo.One solution:
Solution
1import{ useState }from'react'23const start =['Buy milk','Walk the dog','Water the plants']45exportdefaultfunctionTodos(){6const[todos, setTodos]=useState(start)7const[history, setHistory]=useState<string[][]>([])8functionremove(text:string){9setHistory([...history, todos])10setTodos(todos.filter((t)=> t !== text))11}12functionundo(){13setTodos(history[history.length -1])14setHistory(history.slice(0,-1))15}16const rows = todos.map((t)=>(17<likey={t}>18{t}{' '}19<buttonaria-label={'Delete '+ t}onClick={()=>remove(t)}>20 ×21</button>22</li>23))24return(25<div>26<buttononClick={undo}disabled={history.length ===0}>27 Undo28</button>29<ul>{rows}</ul>30</div>31)32}
The history is a list of lists: before each delete, the current todos goes on the end, and Undo takes the last one back off. This only works because filter made a new array and never touched the old one; code that spliced the array in place would have “saved” the same array it then changed. Both setters run in one event, so React renders once with both. The same idea, past and present in one reducer, is how editors build undo and redo.
20. Mastery: building and reviewing a large app · Challenge: write a debounce Hook · Fill the blanksComplete a Hook that returns value only after it has stopped changing for ms milliseconds.1: useState, 2: setTimeout, 3: clearTimeout, 4: msEach new value starts a timer, and the cleanup cancels the previous one, so only a value that stays put for ms gets through. Both value and ms are read by the effect, so both are dependencies. Use it as const q = useDebounced(query, 300) before useRecipes(q).
20. Mastery: building and reviewing a large app · Challenge: a search that waits for you · Code challengeWrite useDebounced(value, ms): it returns value once it has stayed the same for ms milliseconds. Then typing Pea quickly calls search once, with Pea, and lists Pea soup.One solution:
Each new value runs the effect again, and its cleanup clears the timer the last value started, so only a value that stays put for ms reaches setSettled. The search effect depends on settled, not query, so it runs once per pause rather than once per key. Forgetting the cleanup is the classic mistake: every key still searches, just 300 ms late. A real version would also keep the ignore guard from the fetching module, since a slow answer to an old pause can still arrive after a newer one.
20. Mastery: building and reviewing a large app · Challenge: async screens · QuizA recipe page reads use(recipePromise) from a cache, inside <Suspense>. Clicking “next recipe” calls setId inside startTransition. What does the user see while it loads?B. The current recipe stays, with isPending true, until the next one is readyIn a transition, React won’t replace content already on screen with a fallback; it keeps the old recipe and lets you show a pending hint. Without startTransition, the boundary would fall back to its spinner.
20. Mastery: building and reviewing a large app · Challenge: async screens · Put in orderYou are adding a “favourite” button to each recipe that saves to the server. Order a solid implementation.1. Write a Server Function or API call that saves the favourite 2. Wrap the button in a form whose action calls it 3. At the start of that action, show the change at once with useOptimistic 4. Catch a failed save in the action, report it with role="alert", and let the optimistic state roll backStart with the real operation, then wire it into React’s form Actions, then improve how it feels: the optimistic update makes the click land at once, and the error handling needs that action and that optimistic state to exist first. Each layer builds on the one before. A pending hint with useFormStatus fits in at any point once the form exists, which is why it isn’t a step here.
20. Mastery: building and reviewing a large app · Challenge: accessible tabs · Code challengePut the buttons in a role="tablist", give each role="tab", an id and aria-selected, and show only the current tab’s body in one role="tabpanel" with aria-labelledby set to its tab’s id.One solution:
Roles turn plain buttons into a widget a screen reader can describe: “Method, tab, 2 of 2, selected”. The panel takes its name from its tab through aria-labelledby, which is how the test finds it. Showing only the current body is ordinary conditional rendering. A complete version also moves between tabs with the arrow keys and gives only the selected tab tabIndex={0}; the tests here can’t press keys, but real users will.