How React works: a first look
Follow one click through React: components return elements, React renders and compares them, commits only the changes to the DOM, and the browser paints.
- 01Name the steps of an updateFollow a click through trigger, render, commit, and paint, and say who does each step: your code, React, or the browser.
- 02Tell render from commitExplain why a component can render without changing the page, and why React changes only the DOM nodes that differ.
- 03Read an elementDescribe what a component returns: an element, a plain object with a type and props, not a piece of the page.
From a click to the screen
In Why React? you saw how React splits the work: you describe what the screen should show for your data, and React keeps the page matching. This lesson is a first look at how React does its part: what happens between a click and the new screen. Later lessons go deep into each step; here you see the whole path once.
Here is the smallest update there is. Click Add tea a few times.
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p> Items in cart: <strong>{count}</strong> </p> <button onClick={() => setCount(count + 1)}>Add tea</button> </> );}Line 4 gives App a piece of state, count, starting at 0. Lines 8 to 10 describe the paragraph, with the count inside a <strong>. Line 11's button calls setCount(count + 1). Each click raises the number, and nothing else on the page moves. Behind that one number, five things happen:
- Trigger.
setCount(1)tells React the state changed and asks for a new render. - Render. React calls your
Appfunction again. It returns a new description of the screen, where the<strong>holds 1. - Diff. React compares the new description with the one from the last render. Only the number differs: 0 became 1.
- Commit. React changes the page, the DOM, to match: it sets the text inside the
<strong>to 1, and leaves the paragraph's other text and the button alone. - Paint. The browser draws the changed page on the screen.
Steps 2 and 3 happen together: React compares while it renders. So React's docs count the diff as part of the render, and so does the rest of this lesson. That leaves four steps: trigger, render, commit, and paint.
setCount(1) asks for a render, React calls App, compares the new description with the last one, and commits the one difference to the DOM. Then the browser paints it.When you renovate, the architect draws the whole room again, but nobody knocks the house down. The builder compares the new plan with the old one and changes only what's different. Then people walk in and see the result.
- In real life: You ask for a change: one more shelf
- In React: A set function asks for a render
- In real life: The architect draws a new floor plan of the whole room
- In React: Your component returns a new description of its part of the screen
- In real life: The builder lays the new plan over the old one
- In React: React compares the new description with the last one
- In real life: The builder puts up the one new shelf and leaves the walls
- In React: React changes only the DOM nodes that differ
- In real life: Visitors walk in and see the room
- In React: The browser paints the page
A React update happens in steps. A trigger asks for a render. In the render, React calls your components, which return elements: plain objects that describe the screen. React compares them with the last render's. In the commit, it changes only the DOM nodes that differ. Then the browser paints.
Components return a description, not the page
A component never touches the page. It returns elements: small objects that describe what should be there. You can look at them, because they are ordinary JavaScript values:
function ProductRow({ name, quantity }) { return ( <li className="row"> {name} × {quantity} </li> );} const row = <ProductRow name="Tea" quantity={2} />;const item = <li className="row">Tea × 2</li>; export default function App() { return ( <> <pre>{JSON.stringify({ type: typeof row.type, props: row.props }, null, 2)}</pre> <pre>{JSON.stringify({ type: item.type, props: item.props }, null, 2)}</pre> <ul>{row}</ul> </> );}Line 9 writes <ProductRow name="Tea" quantity={2} /> and keeps the result in row. Line 15 prints what kind of value its type is, and its props. It prints "function", because an element for your own component holds the component function itself. The props are { "name": "Tea", "quantity": 2 }. Nothing has run ProductRow yet.
Line 10 makes an element for an HTML tag. Line 16 prints it: its type is the string "li", and its props hold the className and the text, as children. It's a description of a list item, not a list item: it isn't on the page.
Line 17 puts row in a list. That's when React uses it: it calls ProductRow with those props, gets back an <li> element like line 10's, and the commit creates a real <li>, the Tea × 2 under the printed objects. Press Edit & run and change the text on line 10 to Cake × 3: only the second printout changes, because that element is never put on the page.
ProductRow, which returns an element: a plain object with a type and props. Only the commit turns it into a real <li> on the page.Before the code runs, a build tool turns each JSX tag into a call to React's jsx function, which builds these objects; the Writing markup with JSX lesson shows it. Elements are cheap: every render makes new ones and throws the old ones away. React keeps its own record of what it put on the page, and Fiber: React's unit of work, in the last stage of the course, opens it up.
Render: React calls your components
In React, rendering means React calling your components to find out what to show. Two things trigger a render. The first is the app starting: the file that starts every React app calls root.render once.
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
const root = createRoot(document.getElementById("root"));
root.render(<App />);Line 4 hands React the page's empty <div id="root">, and line 5 asks for the first render of App. (Every live example on this page has a file like this; Pop out shows it.) The second trigger is a state update: calling a set function from useState, like setCount. Changing an ordinary variable doesn't trigger anything; React only hears about changes through set functions.
Which components does React call? The one whose state changed, and then every component inside it. Here is a cart made of four kinds of component. Click Add tea:
import { useState } from "react"; function Header({ count }) { return <p>Items: {count}</p>;} function ProductRow({ name, quantity }) { return ( <li> {name} × {quantity} </li> );} function Total({ amount }) { return <p>Total: ${amount}</p>;} const startItems = [ { name: "Tea", price: 3, quantity: 1 }, { name: "Cake", price: 4, quantity: 1 }, { name: "Lemonade", price: 2, quantity: 2 },]; export default function App() { const [items, setItems] = useState(startItems); const count = items.reduce((sum, item) => sum + item.quantity, 0); const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); function addTea() { setItems( items.map((item) => item.name === "Tea" ? { ...item, quantity: item.quantity + 1 } : item, ), ); } return ( <> <Header count={count} /> <ul> {items.map((item) => ( <ProductRow key={item.name} name={item.name} quantity={item.quantity} /> ))} </ul> <Total amount={total} /> <button onClick={addTea}>Add tea</button> <button onClick={() => setItems([...items])}>Same cart again</button> <button onClick={() => setItems(items.slice(0, -1))}>Remove last</button> </> );}Lines 3 to 17 are three small components that only show their props: Header, ProductRow, and Total. App keeps the cart in state on line 26 and works out the count and the total on lines 27 and 28. addTea on lines 30 to 36 makes a new array where Tea has one more, with map and spread from The JavaScript you need for React. Lines 40 to 46 describe the cart with the three components, and lines 47 to 49 are the buttons.
After Add tea, the page shows Items: 5, Tea × 2, and Total: $14. Here's the render behind it. App's state changed, so React calls App. App returns elements for Header, three ProductRows, and Total, so React calls each of them too: six calls in all. Even the Cake row renders, though its props are exactly the same as before. That's normal and usually cheap; the Performance stage shows when it matters, and how memo and the React Compiler skip such calls.
Rendering should only calculate. A component reads its props and state and returns elements; it shouldn't change anything outside itself, like a variable other code reads, or the page. In development, StrictMode calls each component a second time on purpose, to catch components that do. If a click calls several set functions, React waits until your handler finishes and renders once. The Keeping components pure and Render and commit lessons cover these rules in full.
Commit: only the differences reach the DOM
While rendering, React compares each new element with the one in the same place last time. By the end of the render it knows exactly what differs. The commit is when React applies those differences to the DOM, all together. On the very first render there is nothing to compare with, so the commit adds every element to the page.
The playground runs the cart from above, and shows the two steps apart. On the right, Render lists every component React called for your last click, and Commit lists every change it made to the DOM, as a MutationObserver (the browser's tool for watching a part of the page) saw it. On the left, each changed part of the page flashes. Press Add tea, then Same cart again, then Remove last.
click its buttonsItems: 4
- Tea × 1
- Cake × 1
- Lemonade × 2
Total: $11
waiting- Triggernone yet
- Rendernone yet
- Commitnone yet
- Paintnone yet
Renders so far: none
Press Add tea in the cart. The right side lists every component React called and every DOM change it made.
Add tea renders six components and makes three DOM changes: Items: 5, Tea × 2, and Total: $14. Same cart again calls setItems([...items]): a new array with the same items. That's new state, so React renders all six components again, but every new element matches the old one, and the commit changes nothing. Nothing flashes. Remove last renders five components, one row fewer, and makes three changes: the count, the removed Lemonade <li>, and the total.
Same cart again is the clearest proof that render and commit are different steps: a component can render as often as React likes, and the page changes only where the description changed.
Paint: the browser draws the result
React's work ends with the commit. Drawing pixels is the browser's job: when the JavaScript running now finishes, the browser works out the layout and paints the next frame. React's docs say "paint" for this, to keep it apart from React's render. Because the commit makes all its changes before the browser gets its turn, the frame shows them together; you never see the count updated and the total not. The Rendering and the event loop lesson shows when browsers paint.
Each row is one step of an update, who does it, and what happens:
| Step | Who does it | What happens |
|---|---|---|
| Trigger | Your code | A set function, like setItems, asks React for a render. On the first load, root.render(<App />) does. |
| Render | React, calling your components | Each component returns elements. React compares them with the last render's and notes what differs. |
| Commit | React | React changes only the DOM nodes that differ, all in one go. |
| Paint | The browser | The browser draws the changed page in its next frame. |
One thing follows from the order. Inside a click handler, after setCount(count + 1), the page still shows the old number: the render and the commit come after your handler finishes. Exercise 4 lets you see it.
Where the rest of the course goes deeper
Every later stage of the course fills in part of this picture:
- Describing the UI: what goes into the description. Components, JSX, props, conditions, and lists, and why rendering must stay pure.
- Adding interactivity: the triggers. Events, state, render and commit in detail, and why each render sees a fixed snapshot of state.
- Managing state: how React matches old and new elements by their place in the tree, and when it keeps or resets state.
- Refs, effects and custom hooks: code that runs after the commit, and reaching the real DOM nodes.
- Performance: when components render again, and how to skip renders you don't need.
- Under the hood: the same steps in React's own source code: elements, fibers, the work loop, reconciliation, and the commit phase.
Why the note you typed survives
Render and commit explain things you see every day. Type gift wrap in the box below, then click Check stock a few times.
import { useState } from "react"; export default function App() { const [checks, setChecks] = useState(0); return ( <> <p>Green tea: checked {checks} times</p> <input placeholder="Note for the shop" /> <button onClick={() => setChecks(checks + 1)}>Check stock</button> </> );}The count goes up, and your note stays. Each click triggers a render of App. The new description still has an <input> on line 9, in the same place, with the same props. So React keeps the DOM element it made the first time: it doesn't replace it or touch its value, and the text you typed stays in it. On the page, only the number on line 8 changes.
If the description put a different kind of element in that place, React would remove the old one and create a new one, and your text would go with it. Exercise 3 shows that bug, and the Preserving and resetting state lesson explains how React decides what to keep.
- React calls ProductRow for Tea
- React compares the new
<li>description with the old one - Your App adds up the total with
reduce - React sets the Tea row's text to
Tea × 2 - React removes the Lemonade
<li>from the list - On the first render, React adds every element to
<div id="root"> - The browser draws the new numbers on the screen
- The browser shows the next frame, with all three changes at once
Sort each thing that happens when the cart first shows or updates: is it part of the render, the commit, or the paint?
Common mix-ups
- "Rendering means updating the page." No. Rendering is React calling your components. The page changes in the commit, and only where something differs. Same cart again rendered six components and changed nothing.
- "React rebuilds the whole page, or the whole list, on every change." No. The commit touches only the nodes that differ. Add tea changed three text nodes and left the other rows alone.
- "Only the component whose props changed renders." No. When a component renders, every component inside it renders too, unless you skip it on purpose with
memoor the React Compiler does. - "The screen changes as soon as I call setCount." No. A set function asks for a render. React renders and commits after your event handler finishes, and the browser paints after that.
- "The virtual DOM is a copy of the page that React draws." Not quite. "Virtual DOM" is a nickname for the descriptions React compares; React's own docs don't use it. React compares elements, then changes the real DOM, and the browser draws it.
- "A component is the thing on the page." No. A component is a function. It returns elements, and the commit turns them into DOM nodes.
Practice exercises
Predict first, then check: press Edit & run to change the code, and use the playground above to watch renders and DOM changes.
You click Add once. How many components does React call?
import { useState } from "react"; function Header() { return <h3>Tea shop</h3>;} function CartLine({ name }) { return <li>{name}</li>;} function Cart() { const [count, setCount] = useState(0); return ( <> <p>Items: {count}</p> <button onClick={() => setCount(count + 1)}>Add</button> <ul> <CartLine name="Tea" /> <CartLine name="Cake" /> </ul> </> );} export default function App() { return ( <> <Header /> <Cart /> </> );}import { useState } from "react"; function Header() { return <h3>Tea shop</h3>;} function CartLine({ name }) { return <li>{name}</li>;} function Cart() { const [count, setCount] = useState(0); return ( <> <p>Items: {count}</p> <button onClick={() => setCount(count + 1)}>Add</button> <ul> <CartLine name="Tea" /> <CartLine name="Cake" /> </ul> </> );} export default function App() { return ( <> <Header /> <Cart /> </> );}The click changes Cart's state, so React calls Cart and the two CartLines it returns: 3 components. App and Header sit above and beside Cart, so they don't render.
You click Add 10 points once. How many DOM changes does the commit make?
import { useState } from "react"; export default function App() { const [points, setPoints] = useState(0); return ( <> <h3>Asha</h3> <p>Member since 2021</p> <p>Points: {points}</p> <button onClick={() => setPoints(points + 10)}>Add 10 points</button> </> );}import { useState } from "react"; export default function App() { const [points, setPoints] = useState(0); return ( <> <h3>Asha</h3> <p>Member since 2021</p> <p>Points: {points}</p> <button onClick={() => setPoints(points + 10)}>Add 10 points</button> </> );}React renders App again, and the new description differs from the old one in one place: the points number, 0 to 10. The commit changes that one text node; the heading, the other paragraph, and the button stay as they are.
Type gift wrap in the note box and click Check stock: the note is lost. Fix the code so it stays. What does the box hold after a click?
import { useState } from "react"; export default function App() { const [checks, setChecks] = useState(0); return ( <> <p>Checked {checks} times</p> {checks % 2 === 0 ? ( <div> <input placeholder="Note" /> </div> ) : ( <section> <input placeholder="Note" /> </section> )} <button onClick={() => setChecks(checks + 1)}>Check stock</button> </> );}import { useState } from "react"; export default function App() { const [checks, setChecks] = useState(0); return ( <> <p>Checked {checks} times</p> <div> <input placeholder="Note" /> </div> <button onClick={() => setChecks(checks + 1)}>Check stock</button> </> );}With the same <div> and <input> in the same place on every render, React keeps the same DOM input, and the text you typed stays in it: gift wrap.
Click Add tea once. The page then shows 1, but what does line 8 print in the console below the result?
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); console.log("On the page:", document.querySelector("strong").textContent); } return ( <> <p> Items in cart: <strong>{count}</strong> </p> <button onClick={handleClick}>Add tea</button> </> );}import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); console.log("On the page:", document.querySelector("strong").textContent); } return ( <> <p> Items in cart: <strong>{count}</strong> </p> <button onClick={handleClick}>Add tea</button> </> );}setCount(1) only asks for a render. Line 8 runs before React renders or commits, so the <strong> still says 0, and the console shows On the page: 0. Then React renders, commits, and the page shows 1.
Each key press in the search box renders three components, though only the input and its paragraph use query. Move the state into its own component. After your change, how many components render on each key press?
import { useState } from "react"; function ProductGrid() { return ( <ul> <li>Green tea</li> <li>Lemon cake</li> <li>Iced tea</li> </ul> );} function Footer() { return <p>Free delivery over $20</p>;} export default function App() { const [query, setQuery] = useState(""); return ( <> <input placeholder="Search" value={query} onChange={(event) => setQuery(event.target.value)} /> <p>Searching for: {query}</p> <ProductGrid /> <Footer /> </> );}import { useState } from "react"; function SearchBox() { const [query, setQuery] = useState(""); return ( <> <input placeholder="Search" value={query} onChange={(event) => setQuery(event.target.value)} /> <p>Searching for: {query}</p> </> );} function ProductGrid() { return ( <ul> <li>Green tea</li> <li>Lemon cake</li> <li>Iced tea</li> </ul> );} function Footer() { return <p>Free delivery over $20</p>;} export default function App() { return ( <> <SearchBox /> <ProductGrid /> <Footer /> </> );}Now the state lives in SearchBox. A key press triggers a render of SearchBox only: App, ProductGrid, and Footer are outside it, so React doesn't call them. One component renders per key press instead of three.
Add a Clear button that empties the cart with setItems([]). From a fresh cart, how many DOM changes does one click on Clear make?
import { useState } from "react"; function Header({ count }) { return <p>Items: {count}</p>;} function ProductRow({ name, quantity }) { return ( <li> {name} × {quantity} </li> );} function Total({ amount }) { return <p>Total: ${amount}</p>;} const startItems = [ { name: "Tea", price: 3, quantity: 1 }, { name: "Cake", price: 4, quantity: 1 }, { name: "Lemonade", price: 2, quantity: 2 },]; export default function App() { const [items, setItems] = useState(startItems); const count = items.reduce((sum, item) => sum + item.quantity, 0); const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); function addTea() { setItems( items.map((item) => item.name === "Tea" ? { ...item, quantity: item.quantity + 1 } : item, ), ); } return ( <> <Header count={count} /> <ul> {items.map((item) => ( <ProductRow key={item.name} name={item.name} quantity={item.quantity} /> ))} </ul> <Total amount={total} /> <button onClick={addTea}>Add tea</button> <button onClick={() => setItems([...items])}>Same cart again</button> <button onClick={() => setItems(items.slice(0, -1))}>Remove last</button> </> );}import { useState } from "react"; function Header({ count }) { return <p>Items: {count}</p>;} function ProductRow({ name, quantity }) { return ( <li> {name} × {quantity} </li> );} function Total({ amount }) { return <p>Total: ${amount}</p>;} const startItems = [ { name: "Tea", price: 3, quantity: 1 }, { name: "Cake", price: 4, quantity: 1 }, { name: "Lemonade", price: 2, quantity: 2 },]; export default function App() { const [items, setItems] = useState(startItems); const count = items.reduce((sum, item) => sum + item.quantity, 0); const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); function addTea() { setItems( items.map((item) => item.name === "Tea" ? { ...item, quantity: item.quantity + 1 } : item, ), ); } return ( <> <Header count={count} /> <ul> {items.map((item) => ( <ProductRow key={item.name} name={item.name} quantity={item.quantity} /> ))} </ul> <Total amount={total} /> <button onClick={addTea}>Add tea</button> <button onClick={() => setItems([...items])}>Same cart again</button> <button onClick={() => setItems(items.slice(0, -1))}>Remove last</button> <button onClick={() => setItems([])}>Clear</button> </> );}From a fresh cart, Clear makes five DOM changes: Items: 0, three removed <li>s (Tea, Cake, and Lemonade), and Total: $0. The buttons and the empty <ul> stay.
Check your understanding
For each question, ask which step it's about: the trigger, the render, the commit, or the paint.
Question 1 of 7What does "rendering" mean in React?
Choose an answer to see the explanation.
Question 2 of 7What does this button show after three clicks?
Read the code, then predictexport default function App() { let count = 0; function handleClick() { count = count + 1; } return <button onClick={handleClick}>Clicked {count} times</button>; }Choose an answer to see the explanation.
Question 3 of 7After one click on Likes, which components does React call?
Read the code, then predictimport { useState } from "react"; function Header() { return <h3>Tea shop</h3>; } function Footer() { return <p>Open daily</p>; } export default function App() { const [likes, setLikes] = useState(0); return ( <> <Header /> <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button> <Footer /> </> ); }Choose an answer to see the explanation.
Question 4 of 7In the playground, Same cart again makes React call six components. How many DOM changes does the commit make?
Choose an answer to see the explanation.
Question 5 of 7Right after
setCount(count + 1), what does code in the same click handler see on the page?Choose an answer to see the explanation.
Question 6 of 7What is a React element?
Choose an answer to see the explanation.
Question 7 of 7When does the browser paint the new screen?
Choose an answer to see the explanation.
Key takeaways
- An update has four steps: a trigger (a set function, or the first
root.render), a render, a commit, and a paint. - Components return elements: plain objects with a type and props that describe the screen. They aren't on the page.
- Rendering is React calling your components: the one whose state changed, and every component inside it. It only calculates.
- React compares the new elements with the last ones, and the commit changes only the DOM nodes that differ.
- A render can change nothing on the page. Then the commit has nothing to do.
- The browser paints after the commit, so the screen never shows half an update.
Remember the one-liner.
Trigger, render, commit, paint: React calls your components for a description, then changes only what differs.
Coming next: Setting up a React project. It starts a real project on your computer, with the main.jsx and StrictMode you met here, and React DevTools.