Warm-up · Activity 1 of 7
// B4.2 · ~30 min · Beginner
A Vite project from scratch
After this lesson you can create a React project with Vite on your computer, explain which file puts your app on the page, and run, build and preview it.
You will be able to
- Create a Vite React project with npm create vite, on a Node.js version Vite supports
- Explain how index.html, src/main.jsx and src/App.jsx put the app on the page
- Run the dev server, build to dist and preview the build with the npm scripts
Predict · Activity 2 of 7
Predict before you read on. npm run dev is running, and you open http://localhost:5173/. Which file of the project does the browser load first?
Practice · Activity 3 of 7
node --version prints v18.20.4. What does vite.dev say you need for Vite?
Practice · Activity 4 of 7
Match each file or folder of the new project to its job.
Practice · Activity 5 of 7
Fill in the module that src/main.jsx imports createRoot from.
import { StrictMode } from 'react' import { createRoot } from '____' import './index.css' import App from './App.jsx'import { createRoot } from ''Brain teaser · Activity 6 of 7
Brain teaser. Your project's src/main.jsx wraps App in <StrictMode>, as the template does, and npm run dev serves it. App renders two Counter components, and Counter logs in its body. How many lines does the console show when the page loads?
function Counter() { console.log('Counter'); const [count, setCount] = useState(0); return <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>; } function App() { return ( <> <Counter /> <Counter /> </> ); }Apply · Activity 7 of 7
Mini-task: your own Vite project. In a terminal, check node --version, then run npm create vite@latest my-app -- --template react, cd my-app, npm install and npm run dev, and open the Local address it prints. While it runs, replace src/App.jsx with your own component and save: the page updates. Stop the server with Ctrl+C, run npm run build and look into dist, then run npm run preview and open its address.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The template's shape, in the runner
A Vite project keeps its entry in src/main.jsx and its first component in src/App.jsx, and imports ./index.css. Here main.jsx only exports App: the runner does what src/main.jsx does, rendering it into #root inside <StrictMode>, and it applies index.css itself. App.jsx imports a Counter from its own file, with the extension, as files in src/ do. Counter uses the template's updater form setCount((count) => count + 1). The snapshot shows #root after the first render.
main.jsx
import App from './App.jsx';
// In a Vite project, src/main.jsx ends with
// createRoot(document.getElementById('root')).render(
// <StrictMode>
// <App />
// </StrictMode>,
// )
// and imports './index.css'. The runner does both for this default export.
export default App;
App.jsx
import Counter from './Counter.jsx';
export default function App() {
return (
<>
<h1>my-app</h1>
<Counter />
<p>
Edit <code>src/App.jsx</code> and save to test <code>HMR</code>
</p>
</>
);
}
Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
);
}
index.css
body {
font-family: system-ui, sans-serif;
}
Run it with
npm run devOutput
<!-- #root -->
<div id="root"><h1>my-app</h1><button>count is 0</button><p>Edit <code>src/App.jsx</code> and save to test <code>HMR</code></p></div>- <StrictMode> renders no element of its own: #root holds only what App returns.
- Counter.jsx is imported as ./Counter.jsx, with its extension, as in a Vite project's src/ folder.
- index.css is applied by the runner; in Vite, src/main.jsx needs import './index.css' for it.
- In the browser the page stays live: click the button. In a Vite project, saving App.jsx would update it through HMR.
Change it and run it
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.
Exercises
Exercise 1 of 2
Write the entry, as src/main.jsx does
src/main.jsx renders App into the element from index.html. Write that step as a function: start(rootElement) creates a root for rootElement, renders <App /> inside <StrictMode> into it, and returns the root. App.jsx is given. Run shows App through the default export; the tests put an empty element on the page, call start with it, and click the counter.
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.
Hints
Hint 1
createRoot alone displays nothing; the root needs a render call.
Hint 2
Copy the shape of the template: root.render(<StrictMode><App /></StrictMode>);
Hint 3
End the function with return root;
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
// For the runner, which renders this into #root.
export default App;
// What src/main.jsx does, as a function that works for any element.
export function start(rootElement) {
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
return root;
}
Run it on your computer
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 { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
// For the runner, which renders this into #root.
export default App;
// What src/main.jsx does, as a function that works for any element.
export function start(rootElement) {
const root = createRoot(rootElement);
// TODO: render <App /> inside <StrictMode>, then return the root.
}
main.test.jsx
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {act, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {start} from './main.jsx';
// The page of index.html: an empty element for the app.
function indexHtml() {
document.body.innerHTML = '<div id="app-root"></div>';
return document.getElementById('app-root');
}
async function mount(element) {
let root;
await act(async () => {
root = start(element);
});
return root;
}
async function unmount(root) {
if (root && typeof root.unmount === 'function') await act(async () => root.unmount());
}
test('start shows App inside the element', async () => {
const element = indexHtml();
const root = await mount(element);
const heading = within(element).queryByRole('heading', {level: 1});
assert.equal(heading?.textContent, 'my-app', 'the element holds ' + JSON.stringify(element.innerHTML));
await unmount(root);
});
test('The counter works after start', async () => {
const user = userEvent.setup();
const element = indexHtml();
const root = await mount(element);
const button = within(element).queryByRole('button', {name: 'count is 0'});
assert.ok(button !== null, 'there is no button "count is 0" inside the element');
await user.click(button);
await user.click(button);
assert.equal(button.textContent, 'count is 2', 'after two clicks the button reads ' + JSON.stringify(button.textContent));
await unmount(root);
});
test('start returns the root', async () => {
const element = indexHtml();
const root = await mount(element);
assert.equal(typeof root?.render, 'function', 'start returned ' + String(root) + ', not the root from createRoot');
await unmount(root);
});
App.jsx
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<h1>my-app</h1>
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
</>
);
}
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
One component per file
The tea shop's Header and Counter now live in Header.jsx and Counter.jsx, as files in a Vite project's src/ folder would. main.jsx still has an old copy of Counter that cannot count. Import Counter from ./Counter.jsx, delete the old copy, and render, below the header, one Counter labelled Green and one labelled Black. Each must count its own clicks.
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.
Hints
Hint 1
Counter.jsx has a default export, so import it without braces: import Counter from './Counter.jsx';
Hint 2
Two components cannot share one name in a file, so the old function Counter must go.
Hint 3
Pass the label as a prop: <Counter label="Green" /> and <Counter label="Black" />.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import Header from './Header.jsx';
import Counter from './Counter.jsx';
export default function App() {
return (
<>
<Header title="Tea shop" />
<Counter label="Green" />
<Counter label="Black" />
</>
);
}
Run it on your computer
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 Header from './Header.jsx';
// TODO: Counter now lives in Counter.jsx. Import it from there,
// delete this old copy, and render one Counter per tea.
function Counter() {
return <button>Tea: 0</button>;
}
export default function App() {
return (
<>
<Header title="Tea shop" />
<Counter />
</>
);
}
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 App from './main.jsx';
test('The page shows the Tea shop heading and two counters', () => {
render(<App />);
assert.equal(screen.getByRole('heading', {level: 1}).textContent, 'Tea shop', 'the heading');
const labels = screen.queryAllByRole('button').map((button) => button.textContent);
assert.deepEqual(labels, ['Green: 0', 'Black: 0'], 'the buttons read ' + JSON.stringify(labels));
});
test('Green counts its own clicks', async () => {
const user = userEvent.setup();
render(<App />);
const green = screen.queryByRole('button', {name: 'Green: 0'});
assert.ok(green !== null, 'there is no button "Green: 0"');
await user.click(green);
await user.click(green);
assert.equal(green.textContent, 'Green: 2', 'after two clicks Green reads ' + JSON.stringify(green.textContent));
const black = screen.queryByRole('button', {name: /^Black/});
assert.equal(black?.textContent, 'Black: 0', 'clicking Green changed Black to ' + JSON.stringify(black?.textContent));
});
Header.jsx
export default function Header({ title }) {
return <h1>{title}</h1>;
}
Counter.jsx
import { useState } from 'react';
export default function Counter({ label }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((count) => count + 1)}>
{label}: {count}
</button>
);
}
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.
Common mistakes
Leaving out the StrictMode import
import { createRoot } from 'react-dom/client';
export default function App() {
return <h1>my-app</h1>;
}
// Typed from src/main.jsx without its first line, for a second root
const preview = document.createElement('div');
document.body.append(preview);
createRoot(preview).render(
<StrictMode>
<App />
</StrictMode>,
);
What React prints
ReferenceError: StrictMode is not definedWhy, and the fix
<StrictMode> is a component from React, so it must be imported like any other: import { StrictMode } from 'react'. JSX does not import it for you. The run stops with Uncaught ReferenceError before anything renders in that root.
Renaming the component but not its export
import App from './App.jsx';
export default App;
What React prints
ReferenceError: App is not definedWhy, and the fix
export default App; at the end of a file exports a name that must exist in that file. After the rename there is no App in App.jsx, so the module fails while it loads, and nothing renders. Export the new name, export default Shop;, or write export default function Shop() in one go. The file that imports it may keep its own name: import App from './App.jsx' still works.
JSX in a .js file
import Counter from './Counter.js';
export default function App() {
return <Counter />;
}
What React prints
SyntaxError: Unexpected token '<'Why, and the fix
This course's runner turns JSX into JavaScript only in .jsx files, so Counter.js is loaded as plain JavaScript, where < cannot start an expression, and nothing runs. vite.dev names .jsx and .tsx as the files with JSX support, and the template's components are .jsx files. Rename the file to Counter.jsx and import it as ./Counter.jsx. 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
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.