Warm-up · Activity 1 of 7
Warm-up from lesson B1.2: props. What text does App show?
function Greeting({ name, wins }) {
return <p>{name} has {wins + 1} wins</p>;
}
function App() {
return <Greeting name="Ada" wins={2} />;
}// B1.3 · ~30 min · Beginner
After this lesson you handle clicks, keep values in state with useState, and make each tic-tac-toe square show X when you click it.
Warm-up · Activity 1 of 7
function Greeting({ name, wins }) {
return <p>{name} has {wins + 1} wins</p>;
}
function App() {
return <Greeting name="Ada" wins={2} />;
}Predict · Activity 2 of 7
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>Clicked {count} times</button>;
}Practice · Activity 3 of 7
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return <button className="square" onClick=____>{value}</button>;
}Practice · Activity 4 of 7
Practice · Activity 5 of 7
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}
function App() {
return (
<>
<Counter />
<Counter />
</>
);
}Brain teaser · Activity 6 of 7
function Counter() {
let count = 0;
function handleClick() {
count = count + 1;
}
return <button onClick={handleClick}>Clicked {count} times</button>;
}Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
This is the board from react.dev's tutorial after "Making an interactive component". Square no longer gets a value prop; it keeps its value in state, starting at null, so every square starts empty. handleClick calls setValue('X'), and React renders that Square again, now showing X. The run below shows the board before any click. In the browser the page stays live: click a square and it shows X, while the other eight stay empty.
main.jsx
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button className="square" onClick={handleClick}>
{value}
</button>
);
}
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
styles.css
.board-row {
display: flex;
}
.square {
width: 2.5rem;
height: 2.5rem;
margin: 0 -1px -1px 0;
border: 1px solid #999;
background: #fff;
font-size: 1.5rem;
font-weight: bold;
}
Run it with
npm run devOutput
<!-- #root -->
<div id="root"><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div></div>Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Exercise 1 of 2
Make each square remember a click, as in the tutorial. In Square, declare a state variable value with useState, starting at null, show it in the button, and write a handleClick that sets it to 'X'. Pass handleClick to the button's onClick. Keep export in front of Square: the tests also render it on its own.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
At the top of Square: const [value, setValue] = useState(null);
Inside Square, write function handleClick() { setValue('X'); } and show {value} in the button.
Write onClick={handleClick}, with no parentheses after handleClick.
One way to solve it. Yours can look different and still pass the checks.
import { useState } from 'react';
export function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button className="square" onClick={handleClick}>
{value}
</button>
);
}
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.jsx
import { useState } from 'react';
export function Square() {
// TODO: keep a value in state, starting at null, show it, and set it to 'X' on click.
return <button className="square"></button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
main.test.jsx
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Board, {Square} from './main.jsx';
const labels = () => screen.getAllByRole('button').map((button) => button.textContent);
test('All nine squares start empty', () => {
render(<Board />);
assert.deepEqual(labels(), ['', '', '', '', '', '', '', '', ''], 'before any click the squares read ' + JSON.stringify(labels()));
});
test('Clicking a square shows X', async () => {
const user = userEvent.setup();
render(<Square />);
await user.click(screen.getByRole('button'));
const label = screen.getByRole('button').textContent;
assert.equal(label, 'X', 'after one click the square shows ' + JSON.stringify(label));
});
test('Each square has its own state', async () => {
const user = userEvent.setup();
render(<Board />);
await user.click(screen.getAllByRole('button')[4]);
assert.deepEqual(labels(), ['', '', '', '', 'X', '', '', '', ''], 'after a click on the middle square the squares read ' + JSON.stringify(labels()));
});
test('React prints no warnings', async () => {
const warnings = [];
const original = console.error;
console.error = (...args) => warnings.push(args.join(' '));
try {
const user = userEvent.setup();
render(<Board />);
await user.click(screen.getAllByRole('button')[0]);
} finally {
console.error = original;
}
assert.deepEqual(warnings, [], 'console.error while rendering and clicking');
});
styles.css
.board-row {
display: flex;
}
.square {
width: 2.5rem;
height: 2.5rem;
margin: 0 -1px -1px 0;
border: 1px solid #999;
background: #fff;
font-size: 1.5rem;
font-weight: bold;
}
In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.
Run the program:
npm run devThere is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.
Exercise 2 of 2
This is react.dev's Quick Start example with one bug: MyButton counts in a plain variable, so the buttons always show Clicked 0 times. Keep the count in state instead, so each click adds one, and each button counts on its own.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run it and click: the variable changes, but nothing renders again.
Replace let count = 0 with const [count, setCount] = useState(0);
In handleClick, call setCount(count + 1) instead of changing count.
One way to solve it. Yours can look different and still pass the checks.
import { useState } from 'react';
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>Clicked {count} times</button>;
}
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.jsx
import { useState } from 'react';
function MyButton() {
// TODO: a plain variable does not update the screen; keep count in state.
let count = 0;
function handleClick() {
count = count + 1;
}
return <button onClick={handleClick}>Clicked {count} times</button>;
}
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
main.test.jsx
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyApp from './main.jsx';
const labels = () => screen.getAllByRole('button').map((button) => button.textContent);
test('Both buttons start at Clicked 0 times', () => {
render(<MyApp />);
assert.deepEqual(labels(), ['Clicked 0 times', 'Clicked 0 times'], 'the buttons read ' + JSON.stringify(labels()));
});
test('Three clicks on the first button show Clicked 3 times', async () => {
const user = userEvent.setup();
render(<MyApp />);
for (let i = 0; i < 3; i++) await user.click(screen.getAllByRole('button')[0]);
assert.equal(labels()[0], 'Clicked 3 times', 'after three clicks the first button reads ' + JSON.stringify(labels()[0]));
});
test('Each button keeps its own count', async () => {
const user = userEvent.setup();
render(<MyApp />);
const [first, second] = screen.getAllByRole('button');
await user.click(first);
await user.click(first);
await user.click(second);
assert.deepEqual(labels(), ['Clicked 2 times', 'Clicked 1 times'], 'after two clicks on the first and one on the second the buttons read ' + JSON.stringify(labels()));
});
test('React prints no warnings', async () => {
const warnings = [];
const original = console.error;
console.error = (...args) => warnings.push(args.join(' '));
try {
const user = userEvent.setup();
render(<MyApp />);
await user.click(screen.getAllByRole('button')[0]);
} finally {
console.error = original;
}
assert.deepEqual(warnings, [], 'console.error while rendering and clicking');
});
In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.
Run the program:
npm run devThere is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.
import { useState } from 'react';
export default function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return <button onClick={handleClick()}>{value}</button>;
}
What React prints
Too many re-renders. React limits the number of renders to prevent an infinite loop.Why, and the fix
The parentheses call handleClick while Square renders. It sets state, which renders Square again, which calls handleClick again, and React stops the loop with Uncaught Error. Pass the function instead: onClick={handleClick}.
export default function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return <button onClick={handleClick}>{value}</button>;
}
What React prints
ReferenceError: useState is not definedWhy, and the fix
useState comes from the react package, so the file must import it: import { useState } from 'react'; at the top. The run stops with Uncaught ReferenceError as soon as Square renders.
import { useState } from 'react';
const [value, setValue] = useState(null);
export default function Square() {
return <button>{value}</button>;
}
What React prints
Invalid hook call. Hooks can only be called inside of the body of a function component.Why, and the fix
Hooks belong at the top of a component, not at the top of the file. Here useState runs when the file loads, before React renders anything, so React warns and the run stops with TypeError: Cannot read properties of null (reading 'useState'). Move the line into Square, before anything else.
React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.