How React renders
"Render" is one of the most used words in React and one of the least examined. Understanding what actually happens between a state update and the pixels changing on screen clarifies a lot: why components run when they do, why most re-renders cost nothing worth worrying about, and where real performance work should start. This chapter builds that mental model. The two chapters after it, Memoization and Code splitting, put it to work.
A render is a function call
When React "renders" a component, it calls your component function:
function Counter() {
const [count, setCount] = useState(0)
console.log('Counter rendered') // runs every time React calls this function
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}That's the whole trick. The function runs top to bottom, any calculations inside it run again, and it returns JSX describing what the UI should look like right now. Nothing about that involves the browser's DOM yet. A render produces a description; it doesn't touch the page.
That distinction matters because the two things have wildly different costs. Calling a JavaScript function and building a description is fast. Changing the real DOM, which triggers layout and paint work in the browser, is the expensive part. React's whole design leans on doing the cheap thing freely and the expensive thing as little as possible.
The three steps
A single "re-render" moves through three steps. The trigger is usually a state update, though the initial mount, a parent rendering above the component, and a new context value all run the same machinery.
- Render. React calls the component function whose state changed, and then, recursively, every component it returns. The result is a fresh virtual DOM: a lightweight in-memory description of that part of the UI.
- Reconciliation. React runs a diffing algorithm comparing the new virtual DOM against the previous one, working out exactly what changed between them.
- Commit. The differences, and only the differences, are applied to the real DOM. This is the part the user can actually see.
The course offers an analogy worth keeping. An architect updating a building first redraws the blueprint (render), then lists which changes the new blueprint actually requires (reconciliation), and only then does the construction crew touch the building (commit). Redrawing a blueprint is quick. Construction is where the time and money go, so the crew does the least work the list allows.
Rendering and reconciliation stay fast, and the commit stays scoped to what the diff found, which is why React feels quick by default. When something does get slow, the Profiler tells you which step to blame instead of leaving you to guess.
Every click on that Counter button runs all three: the log fires, React diffs the result, and the commit updates the button's text node and nothing else around it.
Rendering is recursive, and that's usually fine
When a component renders, React renders everything it returns, then everything those components return, all the way down each branch before moving to the next. A state change in a parent therefore re-renders all of its descendants by default, whether or not they receive the changed state as props.
That sounds wasteful until you separate the steps again. Those child renders are function calls producing descriptions. During reconciliation, React finds no changed attributes on any of them, so the commit has no DOM work to do for that subtree. The user-visible DOM work stays scoped to what actually changed. Re-rendering a subtree that commits nothing is the normal, healthy case, and for the vast majority of components it is far too cheap to measure.
It stops being fine in two situations: when a component does expensive work during its render, or when a subtree is so large that even cheap renders add up. Those are exactly the cases Memoization addresses. Resist the urge to reach for it before you have evidence, which brings us to measuring.
Measure before you optimize
React is already fast, and reconciliation already keeps DOM updates minimal. The course's advice is blunt: be able to measure a performance problem before applying a technique to fix it, and weigh every optimization against the readability of the code it complicates. Cleverness that nobody can maintain is a cost too.
Two tools cover most measuring needs:
- The browser's Performance tab. Record a session, perform the slow interaction, stop, and inspect what ran and how long it took. Its CPU throttling (try a 4x or 6x slowdown) and network throttling let your fast development machine impersonate the slower devices and connections your users actually have. Problems invisible on your laptop show up immediately.
- The React DevTools Profiler. A browser extension from the React team, living in its own Profiler tab. Same record-act-stop flow, but the output is React-shaped: which components rendered, why they rendered, how long each took, and how long the commit took. For diagnosing React apps specifically, it's usually the more direct tool.
StrictMode: a guardrail for development
StrictMode is a component you wrap around your app (or any subtree) to turn on development-only checks. It's the wrapper Setup puts around <App /> in main.jsx.
In development, StrictMode deliberately double-invokes your component functions, along with the other functions React expects to be pure: useState and useReducer initializers, the updaters you pass to a setter, reducers, and useMemo calculations. It runs each effect an extra time, setting it up, cleaning it up, and setting it up again, and it re-attaches ref callbacks. It also warns about deprecated APIs. In a production build it does nothing at all, so it never slows down or double-renders the app your users see.
Double-running sounds like sabotage until you see what it catches. A component function is supposed to be pure: same inputs, same output, no side effects during render. If rendering twice produces a different result than rendering once, the component was hiding a bug.
The course demonstrates this with a component that calls push on an array defined outside itself during render; the page looks fine until any state change re-runs the function and pushes a duplicate. Under StrictMode the duplicate appears on the very first load, and the fix (copy the array before modifying it, or move the work out of render) follows from it.
Purity covers your updaters
The same purity requirement covers the updater in setCount(c => c + 1), since that function runs twice too.
The effect re-run tests the same contract from the other side. Effects owns the cleanup rule and shows what a missing one looks like under StrictMode, when an uncleared setInterval leaves you counting by twos and leaking a timer per mount.
Seeing more bugs in development feels backwards at first, but that is the entire point. Every bug StrictMode surfaces is one that already existed and would otherwise have waited for production to introduce itself.
Version note
The effect behavior arrived in React 18: on mount, StrictMode runs each effect's setup, then its cleanup, then setup again. Older articles describe StrictMode as only double-rendering components. The course writes the wrapper as React.StrictMode; with named imports it's <StrictMode>.
Wrap your app in StrictMode while you build: it runs things twice on purpose in development to help you spot bugs early, and it switches itself off in the finished app.
Next up: Memoization, where referential equality decides what React can skip.

