Skip to content
aviral gupta

// B3.3 · ~30 min · Beginner

Render and commit

After this lesson you can name the three steps of every screen update in React, count how often a component renders, and predict which DOM nodes a commit changes.

Lesson 3 of 6 in B3 Adding interactivity

You will be able to

  • Name the three steps of a screen update and the two things that trigger a render
  • Explain that rendering means React calls your components, and count the calls under Strict Mode
  • Predict that a commit changes only the DOM nodes whose output differs, so an input keeps its text
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B3.2: state. Lamp keeps two state variables. You click the button three times. What does it show?

    function Lamp() {
      const [on, setOn] = useState(false);
      const [presses, setPresses] = useState(0);
    
      function handleClick() {
        setOn(!on);
        setPresses(presses + 1);
      }
    
      return (
        <button onClick={handleClick}>
          {on ? 'On' : 'Off'}, {presses} presses
        </button>
      );
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. You type milk into the input, then click Tick. Clock renders again with the new time. What do the heading and the input show?

    function Clock() {
      const [time, setTime] = useState('10:00');
    
      return (
        <>
          <h1>{time}</h1>
          <input />
          <button onClick={() => setTime('10:01')}>Tick</button>
        </>
      );
    }
  3. Practice · Activity 3 of 7

    Strict Mode is on, as in this course's runner. Parent mounts, then you click its button once. How many times is Child printed in total?

    function Child() {
      console.log('Child');
      return <p>I have no props</p>;
    }
    
    function Parent() {
      const [count, setCount] = useState(0);
    
      return (
        <>
          <button onClick={() => setCount(count + 1)}>{count}</button>
          <Child />
        </>
      );
    }
  4. Practice · Activity 4 of 7

    Match each piece of code or event to its step.

  5. Practice · Activity 5 of 7

    Fill in the method that triggers the initial render of App into the root.

    const root = createRoot(document.getElementById('root'));
    root.____(<App />);
    root.(<App />);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Before the click, a program keeps the <button> element in a variable, before. After one click, it compares the button on the page with before (===) and reads the text. What does it print?

    function SaveButton() {
      const [saved, setSaved] = useState(false);
    
      return (
        <button onClick={() => setSaved(true)}>
          {saved ? 'Saved' : 'Save'}
        </button>
      );
    }
  7. Apply · Activity 7 of 7

    Mini-task: a stopwatch. In main.jsx, write a Stopwatch component with a seconds state, starting at 0. It shows the seconds in an <h1>, such as 0 s, then a labelled input Lap name, then a +1 s button that adds one second. Run it, type a lap name, and click +1 s a few times. Write down which of the three steps each click goes through, and why the lap name stays.

    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

A clock that keeps your note

The runner triggers the initial render of Clock, as Vite's main.jsx does: createRoot(document.getElementById('root')), then root.render inside <StrictMode>. A run only shows the first render, so two timers at the bottom of main.jsx act like a user: after the first commit they type a note into the input and click Tick, which updates state and triggers a re-render. Then they print the heading, whether the input is still the same DOM node, and its text. In the browser the page stays live: type and click yourself.

main.jsx

import { useState } from 'react';

export default function Clock() {
  const [time, setTime] = useState('10:00');

  return (
    <section>
      <h1>{time}</h1>
      <label>
        Note <input />
      </label>
      <button onClick={() => setTime('10:01')}>Tick</button>
    </section>
  );
}

// Trigger 1, the initial render, is done by the runner, as by Vite's main.jsx:
// createRoot(document.getElementById('root')).render(<StrictMode><Clock /></StrictMode>);

// Act like a user once the page is on screen: type a note, then click Tick.
setTimeout(() => {
  const input = document.querySelector('input');
  input.value = 'Call Ada';
  // Trigger 2: the click handler updates state.
  document.querySelector('button').click();

  setTimeout(() => {
    console.log('h1:', document.querySelector('h1').textContent);
    console.log('same input:', document.querySelector('input') === input);
    console.log('note:', input.value);
  }, 50);
}, 50);

Run it with

npm run dev

Output

h1: 10:01
same input: true
note: Call Ada
<!-- #root -->
<div id="root"><section><h1>10:01</h1><label>Note <input></label><button>Tick</button></section></div>
  • The runner's root.render(...) is the first trigger; createRoot alone only makes the root and displays nothing.
  • The click calls setTime, the second kind of trigger, so React calls Clock again.
  • Only the <h1> text differs, so the commit changes only that text: the input is the same node and keeps Call Ada.
  • The typed text is the input's value, not an attribute, so the snapshot shows <input> without it.
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

Keep the note through Save

Draft shows a heading, a note field and a button. Run it, type a note and click Save: the text is gone, because each branch returns a different wrapper, and React builds a new <input>. Rewrite Draft with one return: a <section> whose className is saved only when saved is true, an <h2> that reads Saved or Draft, the labelled note field, and one button that reads Edit or Save and flips saved. The note must keep its text and stay the same DOM node.

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

    React keeps the <input> only if it appears in the same place in the JSX, with the same parents, on both renders.

  2. Hint 2

    Use one <section>, and switch only what differs: className={saved ? 'saved' : undefined} and {saved ? 'Saved' : 'Draft'}.

  3. Hint 3

    One button can do both jobs: onClick={() => setSaved(!saved)} with the text {saved ? 'Edit' : 'Save'}.

Show a solution

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

import { useState } from 'react';

export default function Draft() {
  const [saved, setSaved] = useState(false);

  return (
    <section className={saved ? 'saved' : undefined}>
      <h2>{saved ? 'Saved' : 'Draft'}</h2>
      <label>
        Note <input />
      </label>
      <button onClick={() => setSaved(!saved)}>{saved ? 'Edit' : 'Save'}</button>
    </section>
  );
}
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';

export default function Draft() {
  const [saved, setSaved] = useState(false);

  // TODO: the two returns put the note field in different wrappers.
  // Type a note and click Save: the text is gone.
  if (saved) {
    return (
      <div className="saved">
        <h2>Saved</h2>
        <label>
          Note <input />
        </label>
        <button onClick={() => setSaved(false)}>Edit</button>
      </div>
    );
  }

  return (
    <section>
      <h2>Draft</h2>
      <label>
        Note <input />
      </label>
      <button onClick={() => setSaved(true)}>Save</button>
    </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 Draft from './main.jsx';

const heading = () => screen.getByRole('heading', {level: 2}).textContent;
const note = () => screen.getByRole('textbox', {name: 'Note'});

test('Save shows Saved, Edit shows Draft again', async () => {
  const user = userEvent.setup();
  render(<Draft />);
  await user.click(screen.getByRole('button', {name: 'Save'}));
  assert.equal(heading(), 'Saved', 'after Save the heading reads ' + JSON.stringify(heading()));
  await user.click(screen.getByRole('button', {name: 'Edit'}));
  assert.equal(heading(), 'Draft', 'after Edit the heading reads ' + JSON.stringify(heading()));
});

test('The note keeps its text after Save', async () => {
  const user = userEvent.setup();
  render(<Draft />);
  await user.type(note(), 'Call Ada');
  await user.click(screen.getByRole('button', {name: 'Save'}));
  assert.equal(note().value, 'Call Ada', 'after Save the note holds ' + JSON.stringify(note().value));
});

test('The note is the same DOM node after Save and Edit', async () => {
  const user = userEvent.setup();
  render(<Draft />);
  const before = note();
  await user.click(screen.getByRole('button', {name: 'Save'}));
  assert.ok(note() === before, 'after Save React put a new <input> on the page');
  await user.click(screen.getByRole('button', {name: 'Edit'}));
  assert.ok(note() === before, 'after Edit React put a new <input> on the page');
});

styles.css

.saved {
  background: #e8f5e9;
}

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

Let React update the clock

Clock shows a time, a tick counter, a note field and a Tick button. Its handler writes the new time into the <h1> by hand, so nothing triggers a render: the counter stays at 0, and a second tick shows the same time again. Change handleTick so that it updates the minutes state instead. Then React renders Clock, and the commit changes the time and the counter while the note keeps its text.

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

    Only a state update triggers a re-render; changing the DOM yourself does not tell React anything.

  2. Hint 2

    The handler only needs one line: setMinutes(minutes + 1);.

  3. Hint 3

    Delete the document.querySelector line: the JSX already shows format(minutes), and React commits the new text.

Show a solution

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

import { useState } from 'react';

function format(minutes) {
  const hours = Math.floor(minutes / 60);
  const rest = minutes % 60;
  return `${hours}:${String(rest).padStart(2, '0')}`;
}

export default function Clock() {
  const [minutes, setMinutes] = useState(600);

  function handleTick() {
    setMinutes(minutes + 1);
  }

  return (
    <section>
      <h1>{format(minutes)}</h1>
      <p>Ticks: {minutes - 600}</p>
      <label>
        Note <input />
      </label>
      <button onClick={handleTick}>Tick</button>
    </section>
  );
}
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';

function format(minutes) {
  const hours = Math.floor(minutes / 60);
  const rest = minutes % 60;
  return `${hours}:${String(rest).padStart(2, '0')}`;
}

export default function Clock() {
  const [minutes, setMinutes] = useState(600);

  function handleTick() {
    // TODO: this writes the DOM by hand, so nothing triggers a render.
    document.querySelector('h1').textContent = format(minutes + 1);
  }

  return (
    <section>
      <h1>{format(minutes)}</h1>
      <p>Ticks: {minutes - 600}</p>
      <label>
        Note <input />
      </label>
      <button onClick={handleTick}>Tick</button>
    </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 Clock from './main.jsx';

const time = () => screen.getByRole('heading', {level: 1}).textContent;
const ticks = () => screen.getByText(/^Ticks:/).textContent;
const tick = () => screen.getByRole('button', {name: 'Tick'});

test('One tick shows 10:01 and Ticks: 1', async () => {
  const user = userEvent.setup();
  render(<Clock />);
  await user.click(tick());
  assert.equal(time(), '10:01', 'after one tick the clock reads ' + JSON.stringify(time()));
  assert.equal(ticks(), 'Ticks: 1', 'after one tick the counter reads ' + JSON.stringify(ticks()));
});

test('Two ticks show 10:02', async () => {
  const user = userEvent.setup();
  render(<Clock />);
  await user.click(tick());
  await user.click(tick());
  assert.equal(time(), '10:02', 'after two ticks the clock reads ' + JSON.stringify(time()));
});

test('The note keeps its node and its text while the clock ticks', async () => {
  const user = userEvent.setup();
  render(<Clock />);
  const note = screen.getByRole('textbox', {name: 'Note'});
  await user.type(note, 'Call Ada');
  await user.click(tick());
  assert.ok(screen.getByRole('textbox', {name: 'Note'}) === note, 'after a tick React put a new <input> on the page');
  assert.equal(note.value, 'Call Ada', 'after a tick the note holds ' + JSON.stringify(note.value));
});

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

Rendering into an element that is not on the page

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

function Banner() {
  return <p>Sale today</p>;
}

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

// A second root for a banner outside the app
createRoot(document.getElementById('banner')).render(<Banner />);

What React prints

Target container is not a DOM element.

Why, and the fix

A page may have several React roots, but each needs a DOM node. This page has only <div id="root">, so getElementById('banner') returns null, and the run stops with Uncaught Error. Check the id against the HTML, or add the element first. The error also appears if the script runs before the element exists. For one app, one root is enough: render <Banner /> inside App.

Using ReactDOM.render from an old tutorial

import * as ReactDOM from 'react-dom';

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

ReactDOM.render(<App />, document.getElementById('root'));

What React prints

TypeError: ReactDOM.render is not a function

Why, and the fix

ReactDOM.render was deprecated in React 18 and removed in React 19, so the run stops with Uncaught TypeError (Chrome and Node wording; Firefox and Safari word it differently). Import createRoot from 'react-dom/client', then call createRoot(document.getElementById('root')).render(<App />).

Creating the root again in the component file

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

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

createRoot(document.getElementById('root')).render(<App />);

What React prints

You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before.

Why, and the fix

Like Vite's main.jsx, this course's runner already creates the root for the default export, so the container gets a second root and React warns. Create one root per container: either export the component and let the runner (or main.jsx in Vite) render it, or remove the default export. To update the screen later, set state; do not create a root again.

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

Step 1: something triggers a render

Every screen update in React has three steps: trigger, render, commit. There are only two triggers. The first is the initial render: createRoot(document.getElementById('root')) makes a root for a DOM node, and root.render(<App />) asks React to show App in it. Vite's main.jsx and this course's runner do that for you; until someone calls root.render, nothing is displayed. The second trigger is a state update: once a component is on screen, calling a set function such as setTime('10:01') queues a render of that component.

Step 2: React renders, which means it calls your components

Rendering is React calling your components. On the initial render, React calls the root component. On a later render, it calls the component whose state update triggered the render. The process is recursive: if that component returns other components, React calls them too, and their children, until it knows exactly what should be on screen. So a child with no props renders again when its parent does. Rendering must be a pure calculation, as in lesson B2.6: in development, Strict Mode calls each component twice to catch impure code, so a console.log in a body prints two lines per render.

Step 3: React commits, then the browser paints

After rendering, React changes the DOM. On the initial render it puts all the new nodes on screen with appendChild(). On a re-render it applies only the minimal changes it worked out while rendering: React only changes the DOM nodes if there is a difference between renders. When a clock's <h1> gets a new time, React changes that text and leaves the <input> next to it alone: same node, and the text you typed stays. If the result is the same as last time, React does not touch the DOM at all. Then the browser repaints the screen; react.dev calls that painting.

Sources

Last reviewed October 5, 2026