Skip to content
aviral gupta

// B4.1 · ~30 min · Beginner

Installation: a framework or from scratch

After this lesson you know the ways to start with React that react.dev lists today, can pick one for a project, and can add React to part of an existing page.

Lesson 1 of 4 in B4 Setting up a project

Start of the module

You will be able to

  • Try React without installing anything, and say what this course's runner does like a Vite project
  • Choose between a framework and a build tool from scratch, naming the options react.dev lists
  • Add React to part of an existing page with createRoot, one root per piece of UI
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B3.6: the cart's updater functions. You click the button once. What does it show?

    function AddTwice() {
      const [quantity, setQuantity] = useState(0);
    
      function handleClick() {
        setQuantity((q) => q + 1);
        setQuantity((q) => q + 1);
      }
    
      return <button onClick={handleClick}>Quantity: {quantity}</button>;
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. An existing page's <body> holds <p>Welcome to the shop</p><nav id="navigation"></nav>. Its script runs the code below. Which elements does <body> hold afterwards? Each element is listed, with its first child after >.

    function NavigationBar() {
      return <h1>Hello from React!</h1>;
    }
    
    const domNode = document.getElementById('navigation');
    const navRoot = createRoot(domNode);
    navRoot.render(<NavigationBar />);
  3. Practice · Activity 3 of 7

    You want a new React app with routing built in, using React Router as a framework. Which command does react.dev show for it?

  4. Practice · Activity 4 of 7

    Match each name from react.dev's installation pages to what it is.

  5. Practice · Activity 5 of 7

    The existing page's HTML has <nav id="navigation"></nav>. Fill in the argument so that NavigationBar renders inside it.

    // The page's HTML: <nav id="navigation"></nav>
    const domNode = document.getElementById('____');
    const navRoot = createRoot(domNode);
    navRoot.render(<NavigationBar />);
    document.getElementById('');
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The page has <div id="top"> and <div id="bottom">, and each gets its own root with a LikeButton. You click the button in #top once. What do #top and #bottom show?

    function LikeButton() {
      const [liked, setLiked] = useState(false);
    
      return (
        <button onClick={() => setLiked(true)}>
          {liked ? 'Liked' : 'Like'}
        </button>
      );
    }
    
    createRoot(document.getElementById('top')).render(<LikeButton />);
    createRoot(document.getElementById('bottom')).render(<LikeButton />);
  7. Apply · Activity 7 of 7

    Mini-task: one component, three places. Write the Greeting and App below in this course's main.jsx and run it. Then open react.dev's Installation page and edit its sandbox (its file is App.js), and, if you like, open vite.new/react, which vite.dev names for trying a Vite template online. Change the name to yours in each. For each place, write down what you installed, which file holds the component, and what puts it on the page.

    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

One app in #root, one widget in a <nav>

Two ways React gets onto a page. The default export, App, is the whole app: the runner renders it into #root, as the src/main.jsx of a Vite project does. Below it, main.jsx behaves like a script on an existing page: it finds <nav id="navigation"> and gives it its own root for NavigationBar, as react.dev's page on adding React to an existing project does. The runner's page has only #root, so the code first adds the <nav> an existing page would already have. The snapshot shows the whole <body>.

main.jsx

import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import NavigationBar from './NavigationBar.jsx';

// 1. The app. The runner renders this default export into #root inside
//    <StrictMode>, as the src/main.jsx of a Vite project does with
//    createRoot(document.getElementById('root')).render(<App />).
export default App;

// 2. A widget on an existing page. Its HTML would already contain
//    <nav id="navigation"></nav>; the runner's page has only #root, so add it.
const nav = document.createElement('nav');
nav.id = 'navigation';
document.body.prepend(nav);

const domNode = document.getElementById('navigation');
const navRoot = createRoot(domNode);
navRoot.render(<NavigationBar />);

App.jsx

export default function App() {
  return (
    <main>
      <h1>Bookshop</h1>
      <p>The whole app lives in #root.</p>
    </main>
  );
}

NavigationBar.jsx

export default function NavigationBar() {
  return (
    <ul>
      <li>Books</li>
      <li>Cart</li>
    </ul>
  );
}

Run it with

npm run dev

Output

<!-- body -->
<body><nav id="navigation"><ul><li>Books</li><li>Cart</li></ul></nav><div id="root"><main><h1>Bookshop</h1><p>The whole app lives in #root.</p></main></div></body>
  • Two roots, two trees: App in #root and NavigationBar in the <nav>. Neither knows about the other.
  • The runner, like Vite's main.jsx, calls createRoot for #root; the code calls it once more for the widget, on a different element.
  • React fills only the elements it was given. Everything else on an existing page stays as its HTML made it.
  • App.jsx and NavigationBar.jsx are imported with their extension, as in a Vite project's src/ folder.
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

A like button on an existing page

A shop page that is not built with React wants a like button in its <div id="like"></div>. Complete mountLikeButton(domNode): create a root for domNode, render <LikeButton /> into it, and return the root, so that the page can remove the widget later with root.unmount(). Run shows Page; the tests build the existing page's HTML and call your function.

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
  1. Hint 1

    createRoot(domNode) makes a root for the element; nothing shows until you call render on it.

  2. Hint 2

    Keep the root in a variable: const root = createRoot(domNode);

  3. Hint 3

    Then root.render(<LikeButton />); and return root;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';
import { createRoot } from 'react-dom/client';

function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(true)}>
      {liked ? 'Liked' : 'Like'}
    </button>
  );
}

export default function Page() {
  return <h1>Product page</h1>;
}

// The existing page calls this with its <div id="like"></div>.
export function mountLikeButton(domNode) {
  const root = createRoot(domNode);
  root.render(<LikeButton />);
  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 { useState } from 'react';
import { createRoot } from 'react-dom/client';

function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(true)}>
      {liked ? 'Liked' : 'Like'}
    </button>
  );
}

export default function Page() {
  return <h1>Product page</h1>;
}

// The existing page calls this with its <div id="like"></div>.
export function mountLikeButton(domNode) {
  // TODO: create a root for domNode, render <LikeButton /> into it,
  // and 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 {mountLikeButton} from './main.jsx';

// The HTML of the existing page: some text, and an empty element for the widget.
function existingPage() {
  document.body.innerHTML = '<p>Made by the shop team</p><div id="like"></div>';
  return document.getElementById('like');
}

async function mount(domNode) {
  let root;
  await act(async () => {
    root = mountLikeButton(domNode);
  });
  return root;
}

async function unmount(root) {
  if (root && typeof root.unmount === 'function') await act(async () => root.unmount());
}

test('The Like button appears inside #like', async () => {
  const domNode = existingPage();
  const root = await mount(domNode);
  const button = within(domNode).queryByRole('button', {name: 'Like'});
  assert.ok(button !== null, '#like holds ' + JSON.stringify(domNode.innerHTML) + ', not a Like button');
  await unmount(root);
});

test('A click changes the button to Liked', async () => {
  const user = userEvent.setup();
  const domNode = existingPage();
  const root = await mount(domNode);
  const button = within(domNode).queryByRole('button', {name: 'Like'});
  assert.ok(button !== null, 'there is no Like button inside #like to click');
  await user.click(button);
  assert.equal(button.textContent, 'Liked', 'after a click the button reads ' + JSON.stringify(button.textContent));
  await unmount(root);
});

test('mountLikeButton returns the root and keeps the rest of the page', async () => {
  const domNode = existingPage();
  const root = await mount(domNode);
  assert.equal(typeof root?.unmount, 'function', 'mountLikeButton returned ' + String(root) + ', not the root from createRoot');
  assert.equal(document.querySelector('p')?.textContent, 'Made by the shop team', 'the page text outside #like changed');
  await unmount(root);
});

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 dev

There 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

Two widgets, two roots

The shop page has <span id="cart-badge"> in its header and <div id="newsletter"> in its footer. mountWidgets should show CartBadge with a count of 2 in the first and Newsletter in the second. Right now it renders both into the same root, so the badge is replaced by the newsletter and the footer stays empty. Give each element its own root and return both roots in an array.

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
  1. Hint 1

    Calling render again on the same root replaces what it showed before.

  2. Hint 2

    Call createRoot a second time, with document.getElementById('newsletter').

  3. Hint 3

    Return [badgeRoot, newsletterRoot] so the page can unmount each widget.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { createRoot } from 'react-dom/client';

function CartBadge({ count }) {
  return <strong>Cart: {count}</strong>;
}

function Newsletter() {
  return (
    <section>
      <h2>Newsletter</h2>
      <button>Subscribe</button>
    </section>
  );
}

export default function Page() {
  return <h1>Shop</h1>;
}

// The existing page has <span id="cart-badge"></span> in its header
// and <div id="newsletter"></div> in its footer.
export function mountWidgets() {
  const badgeRoot = createRoot(document.getElementById('cart-badge'));
  badgeRoot.render(<CartBadge count={2} />);

  const newsletterRoot = createRoot(document.getElementById('newsletter'));
  newsletterRoot.render(<Newsletter />);

  return [badgeRoot, newsletterRoot];
}
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 { createRoot } from 'react-dom/client';

function CartBadge({ count }) {
  return <strong>Cart: {count}</strong>;
}

function Newsletter() {
  return (
    <section>
      <h2>Newsletter</h2>
      <button>Subscribe</button>
    </section>
  );
}

export default function Page() {
  return <h1>Shop</h1>;
}

// The existing page has <span id="cart-badge"></span> in its header
// and <div id="newsletter"></div> in its footer.
export function mountWidgets() {
  const root = createRoot(document.getElementById('cart-badge'));
  root.render(<CartBadge count={2} />);
  root.render(<Newsletter />);
  return [root];
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {act, within} from '@testing-library/react';
import {mountWidgets} from './main.jsx';

function existingPage() {
  document.body.innerHTML =
    '<header>Shop <span id="cart-badge"></span></header><main>Books</main><footer><div id="newsletter"></div></footer>';
}

async function mount() {
  let roots;
  await act(async () => {
    roots = mountWidgets();
  });
  return Array.isArray(roots) ? roots : [];
}

async function unmountAll(roots) {
  for (const root of roots) if (root && typeof root.unmount === 'function') await act(async () => root.unmount());
}

test('The cart badge shows Cart: 2', async () => {
  existingPage();
  const roots = await mount();
  const badge = document.getElementById('cart-badge');
  assert.equal(badge.textContent, 'Cart: 2', '#cart-badge reads ' + JSON.stringify(badge.textContent));
  await unmountAll(roots);
});

test('The newsletter appears inside #newsletter', async () => {
  existingPage();
  const roots = await mount();
  const footer = document.getElementById('newsletter');
  const button = within(footer).queryByRole('button', {name: 'Subscribe'});
  assert.ok(button !== null, '#newsletter holds ' + JSON.stringify(footer.innerHTML) + ', not the Subscribe button');
  await unmountAll(roots);
});

test('mountWidgets returns one root per widget', async () => {
  existingPage();
  const roots = await mount();
  assert.equal(roots.length, 2, 'mountWidgets returned ' + roots.length + ' root(s)');
  assert.ok(roots[0] !== roots[1], 'both entries are the same root');
  await unmountAll(roots);
});

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 dev

There 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

Passing the component to createRoot

import { createRoot } from 'react-dom/client';

function NavigationBar() {
  return <p>Books and cart</p>;
}

export default function App() {
  return <h1>Bookshop</h1>;
}

const nav = document.createElement('nav');
document.body.prepend(nav);

// The order of the old ReactDOM.render(element, container), in reverse
createRoot(nav, <NavigationBar />);

What React prints

You passed a JSX element to createRoot. You probably meant to call root.render instead.

Why, and the fix

createRoot takes only the DOM element, plus optional options; it renders nothing. React warns, and the <nav> stays empty. Keep the root and render into it: const navRoot = createRoot(nav); navRoot.render(<NavigationBar />);

Passing the element to root.render again

import { createRoot } from 'react-dom/client';

function NavigationBar() {
  return <p>Books and cart</p>;
}

export default function App() {
  return <h1>Bookshop</h1>;
}

const nav = document.createElement('nav');
document.body.prepend(nav);

const navRoot = createRoot(nav);
navRoot.render(<NavigationBar />, nav);

What React prints

You passed a container to the second argument of root.render(...).

Why, and the fix

The root already knows its element: you passed it to createRoot. root.render takes one argument, the JSX to show, so React warns about the second one. Write navRoot.render(<NavigationBar />); old tutorials that pass the element twice were written for ReactDOM.render, which React 19 removed.

Calling render on the DOM element

function NavigationBar() {
  return <p>Books and cart</p>;
}

export default function App() {
  return <h1>Bookshop</h1>;
}

const nav = document.createElement('nav');
nav.id = 'navigation';
document.body.prepend(nav);

document.getElementById('navigation').render(<NavigationBar />);

What React prints

TypeError: document.getElementById(...).render is not a function

Why, and the fix

A DOM element has no render method; a React root has. The run stops with Uncaught TypeError (Chrome and Node.js wording; Firefox and Safari word it differently). Import createRoot from 'react-dom/client', make a root for the element, then call render on the root: createRoot(document.getElementById('navigation')).render(<NavigationBar />);

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.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Try React without installing anything

React was designed for gradual adoption: you can use as little or as much of it as you need. To try it, you need nothing installed. react.dev's pages have sandboxes, and CodeSandbox, StackBlitz and CodePen run React online. This course's runner works like a Vite project: the page is an empty <div id="root">, and your main.jsx's default export is rendered into it inside <StrictMode>, as Vite's src/main.jsx does with createRoot. One old way is closed: react.dev says Create React App has been deprecated, so do not start a project with it.

A new app: start with a framework

For a new app or website, react.dev recommends starting with a framework. It lists Next.js (App Router), npx create-next-app@latest; React Router (v7), npx create-react-router@latest; and Expo for native Android and iOS apps, npx create-expo-app@latest. TanStack Start (Beta) and RedwoodSDK are listed as up-and-coming. These frameworks do not require a server: they support client-side rendering, single-page apps and static-site generation, and you can add server rendering later, per route. They come with solutions for routing, data fetching and other common patterns.

From scratch, or into a page you already have

Building from scratch starts with a build tool: Vite, Parcel or Rsbuild. It gives you a development server and a build command, and starts you with a client-only single-page app. Routing, data fetching and code-splitting are then yours to choose; react.dev suggests libraries such as React Router, TanStack Query and SWR. Pick this route only if you are comfortable solving those problems yourself. For an existing page, you do not rewrite it: give an element a unique id and call createRoot on it, one root per piece of UI that React manages. An app fully built with React usually has one root.

Sources

Last reviewed October 5, 2026