Warm-up · Activity 1 of 7
Warm-up from module B1: a component, a prop and curly braces. What text does Page show?
function Greeting({ name }) {
return <p>Hello, {name}</p>;
}
function Page() {
return <Greeting name="Ada" />;
}// B2.1 · ~30 min · Beginner
After this lesson you define and nest components the way react.dev does, and split them into files with default and named exports.
Lesson 1 of 6 in B2 Describing the UI
You will be able to
Warm-up · Activity 1 of 7
function Greeting({ name }) {
return <p>Hello, {name}</p>;
}
function Page() {
return <Greeting name="Ada" />;
}Predict · Activity 2 of 7
function Profile() {
return
<img src="ada.jpg" alt="Ada" />;
}
function Gallery() {
return <section><Profile /></section>;
}Practice · Activity 3 of 7
export ____ function Gallery() {
return <h1>Gallery</h1>;
}Practice · Activity 4 of 7
Practice · Activity 5 of 7
// Gallery.jsx
export default function Gallery() {
return <section>…</section>;
}
export function Profile() {
return <img src="ada.jpg" alt="Ada Lovelace" />;
}Brain teaser · Activity 6 of 7
function Page() {
const [dark, setDark] = useState(false);
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
return (
<>
<Counter />
<button onClick={() => setDark(!dark)}>Theme</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
react.dev's gallery, split into files. Profile.jsx default-exports Profile, one picture. Gallery.jsx imports it and renders it three times; it default-exports Gallery and adds a named export, Heading. main.jsx imports all three: the default exports without braces, Heading in braces, each path with its extension. Its own default export, App, is what the runner renders into #root. The pictures do not load in the runner; the snapshot shows the HTML.
main.jsx
import Gallery from './Gallery.jsx';
import {Heading} from './Gallery.jsx';
import Profile from './Profile.jsx';
export default function App() {
return (
<>
<Heading />
<Gallery />
<h2>Scientist of the month</h2>
<Profile />
</>
);
}
Gallery.jsx
import Profile from './Profile.jsx';
export function Heading() {
return <h1>Amazing scientists</h1>;
}
export default function Gallery() {
return (
<section>
<Profile />
<Profile />
<Profile />
</section>
);
}
Profile.jsx
export default function Profile() {
return (
<img
src="katherine-johnson.jpg"
alt="Katherine Johnson"
/>
);
}
Run it with
npm run devOutput
<!-- #root -->
<div id="root"><h1>Amazing scientists</h1><section><img alt="Katherine Johnson" src="katherine-johnson.jpg"><img alt="Katherine Johnson" src="katherine-johnson.jpg"><img alt="Katherine Johnson" src="katherine-johnson.jpg"></section><h2>Scientist of the month</h2><img alt="Katherine Johnson" src="katherine-johnson.jpg"></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
TeamPage used to define everything in main.jsx. Title, Avatar and Footer have been moved into files: Avatar.jsx default-exports Avatar, and Labels.jsx has the named exports Title and Footer. Finish the split: delete the old copies from main.jsx, import the components from the files the matching way, and keep rendering Title, Avatar and Footer in <main>, in that order.
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.
Open Avatar.jsx and Labels.jsx: is each component exported with default or without?
A default export is imported without braces: import Avatar from './Avatar.jsx';
Two named exports from one file go in one pair of braces: import {Title, Footer} from './Labels.jsx';
One way to solve it. Yours can look different and still pass the checks.
import Avatar from './Avatar.jsx';
import {Title, Footer} from './Labels.jsx';
export default function TeamPage() {
return (
<main>
<Title />
<Avatar />
<Footer />
</main>
);
}
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
// The components now live in Avatar.jsx and Labels.jsx. Finish the split:
// delete these old copies and import the new ones instead.
function Title() {
return <h1>Team</h1>;
}
function Avatar() {
return <img className="avatar" src="ada.jpg" alt="Ada" />;
}
function Footer() {
return <p>Made by us</p>;
}
export default function TeamPage() {
return (
<main>
<Title />
<Avatar />
<Footer />
</main>
);
}
main.test.jsx
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import TeamPage from './main.jsx';
test('The heading comes from Title in Labels.jsx', () => {
render(<TeamPage />);
const heading = screen.getByRole('heading', {level: 1}).textContent;
assert.equal(heading, 'Our team', 'the heading reads ' + JSON.stringify(heading));
});
test('The picture comes from Avatar in Avatar.jsx', () => {
render(<TeamPage />);
const alt = screen.getByRole('img').getAttribute('alt');
assert.equal(alt, 'Ada Lovelace', 'the picture has alt=' + JSON.stringify(alt));
});
test('The footer comes from Footer in Labels.jsx', () => {
render(<TeamPage />);
assert.ok(screen.queryByText('Made with React'), 'no element shows the text Made with React');
});
test('Title, Avatar and Footer sit in <main> in this order', () => {
render(<TeamPage />);
const tags = [...screen.getByRole('main').children].map((child) => child.tagName.toLowerCase());
assert.deepEqual(tags, ['h1', 'img', 'p'], '<main> holds ' + JSON.stringify(tags));
});
test('React prints no warnings', () => {
const warnings = [];
const original = console.error;
console.error = (...args) => warnings.push(args.join(' '));
try {
render(<TeamPage />);
} finally {
console.error = original;
}
assert.deepEqual(warnings, [], 'console.error while rendering');
});
Avatar.jsx
export default function Avatar() {
return <img className="avatar" src="ada-lovelace.jpg" alt="Ada Lovelace" />;
}
Labels.jsx
export function Title() {
return <h1>Our team</h1>;
}
export function Footer() {
return <p>Made with React</p>;
}
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
Gallery shows one photo with a LikeButton, and LikeButton is defined inside Gallery. Click Like, then Dark mode: the likes are lost. Move LikeButton to the top level of the file, and show three photos (Sunrise, Forest and Harbour, each an <img> with that alt text), each with its own <LikeButton />. Keep the Dark mode button first.
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 the starter, click Like twice, then Dark mode, and watch the count.
Cut the whole function LikeButton() { … } and paste it above export default function Gallery().
Each photo is a <figure> with an <img> and a <LikeButton />; write three of them.
One way to solve it. Yours can look different and still pass the checks.
import {useState} from 'react';
function LikeButton() {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}
export default function Gallery() {
const [dark, setDark] = useState(false);
return (
<section className={dark ? 'dark' : 'light'}>
<button onClick={() => setDark(!dark)}>Dark mode</button>
<figure>
<img src="sunrise.jpg" alt="Sunrise" />
<LikeButton />
</figure>
<figure>
<img src="forest.jpg" alt="Forest" />
<LikeButton />
</figure>
<figure>
<img src="harbour.jpg" alt="Harbour" />
<LikeButton />
</figure>
</section>
);
}
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 default function Gallery() {
const [dark, setDark] = useState(false);
// TODO: move LikeButton out of Gallery, to the top level of the file.
function LikeButton() {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}
// TODO: show three photos, each with its own LikeButton.
return (
<section className={dark ? 'dark' : 'light'}>
<button onClick={() => setDark(!dark)}>Dark mode</button>
<figure>
<img src="sunrise.jpg" alt="Sunrise" />
<LikeButton />
</figure>
</section>
);
}
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 Gallery from './main.jsx';
const likes = () => screen.getAllByRole('button', {name: /^Like/}).map((button) => button.textContent);
test('Three photos, each with a like button', () => {
render(<Gallery />);
const photos = screen.getAllByRole('img').length;
assert.equal(photos, 3, 'the gallery shows ' + photos + ' photo(s)');
assert.deepEqual(likes(), ['Like (0)', 'Like (0)', 'Like (0)'], 'the like buttons read ' + JSON.stringify(likes()));
});
test('Each like button counts on its own', async () => {
const user = userEvent.setup();
render(<Gallery />);
const first = screen.getAllByRole('button', {name: /^Like/})[0];
await user.click(first);
await user.click(first);
assert.deepEqual(likes(), ['Like (2)', 'Like (0)', 'Like (0)'], 'after two clicks on the first: ' + JSON.stringify(likes()));
});
test('Likes survive a click on Dark mode', async () => {
const user = userEvent.setup();
render(<Gallery />);
await user.click(screen.getAllByRole('button', {name: /^Like/})[0]);
await user.click(screen.getByRole('button', {name: 'Dark mode'}));
assert.equal(likes()[0], 'Like (1)', 'after one like and Dark mode, the first button reads ' + JSON.stringify(likes()[0]));
});
test('React prints no warnings', () => {
const warnings = [];
const original = console.error;
console.error = (...args) => warnings.push(args.join(' '));
try {
render(<Gallery />);
} finally {
console.error = original;
}
assert.deepEqual(warnings, [], 'console.error while rendering');
});
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 Gallery from './Gallery.jsx';
export default function App() {
return <Gallery />;
}
What React prints
does not provide an export named 'default'Why, and the fix
The browser's whole line reads SyntaxError: The requested module './Gallery.jsx' does not provide an export named 'default', and nothing runs. Gallery.jsx exports Gallery by name, so import it in braces: import {Gallery} from './Gallery.jsx'. Or make it the file's default with export default function Gallery(). The wording is V8's (Chrome, Node); Firefox and Safari word it differently.
import {Gallery} from './Gallery.jsx';
export default function App() {
return <Gallery />;
}
What React prints
does not provide an export named 'Gallery'Why, and the fix
Braces ask for an export with that exact name, but Gallery.jsx has only a default export. The full line is a SyntaxError naming the module './Gallery.jsx'. Drop the braces: import Gallery from './Gallery.jsx'. react.dev: you will get an error if you try to import a default export the same way you would a named export. The wording is V8's; Firefox and Safari word it differently.
import Gallery from './Gallery.jsx';
export default function App() {
return <Gallery />;
}
What React prints
SyntaxError: Duplicate export of 'default'Why, and the fix
A file can have only one default export, so Gallery.jsx does not load, and nothing runs. Keep export default on the main component, Gallery, and make Profile a named export, export function Profile(), or drop its export if only Gallery uses it. The wording is V8's; Firefox and Safari word it differently.
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.