Skip to content
React rc rendering 4 min read

How Rendering Works

“Rendering” is one of the most misunderstood words in React. It does not mean “updating the screen” — it means React calling your component function to figure out what the UI should look like. Whether that result actually touches the DOM is a separate decision React makes afterward. Understanding the difference between a render and a DOM update is the key to reasoning about performance, effects, and why a component re-runs more often than you expected.

What triggers a render

A render is React invoking your component function to produce a new description of the UI (a tree of React elements). The very first render is the initial mount; every render after that is a re-render. There are exactly four things that cause a component to render:

TriggerWhat happens
Initial mountReact calls the component for the first time when it enters the tree
State changeA useState/useReducer setter is called with a new value
Props changeThe parent passes new props during its own re-render
Context changeA consumed Context.Provider value changes

There is a crucial implication of the third row: when a parent re-renders, its children re-render by default — even if their props did not change. React does not diff props to decide whether to call a child; it calls the child and then decides whether the result changes the DOM.

import { useState } from "react";

function Child() {
  console.log("Child rendered");
  return <p>I have no props and no state.</p>;
}

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child />
    </div>
  );
}

Output:

Child rendered      // initial mount
Child rendered      // logged again on every click, even though Child never changed

Child re-renders on each click purely because Parent re-rendered. This is normal and usually cheap — rendering is fast. It only becomes a problem for expensive subtrees, which is what React.memo and useMemo exist to address.

Tip: Calling a state setter with a value that is Object.is-equal to the current value will bail out of the re-render for that component. setCount(count) when count is already 5 does nothing.

The render phase

The render phase is pure and side-effect free. React walks the component tree, calls each function component, and collects the returned elements to build a new virtual tree. During this phase React must be able to call your component any number of times, pause it, or throw the result away — so your render logic must not mutate external state, perform fetches, or write to the DOM directly.

function PriceTag({ amount, currency }) {
  // Pure computation during render — derived entirely from props.
  const formatted = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
  }).format(amount);

  return <span className="price">{formatted}</span>;
}

Once the new tree is built, React compares it against the previous tree (this diffing step is reconciliation) to work out the minimal set of changes. No DOM has been touched yet at this point.

The commit phase

The commit phase is where React applies the calculated changes to the actual DOM. This is the only phase that mutates the host environment — inserting, updating, and removing DOM nodes, updating refs, and then running effects. useLayoutEffect callbacks fire synchronously after the DOM mutation but before the browser paints; useEffect callbacks fire asynchronously after paint.

        ┌─────────────────────── RENDER PHASE ───────────────────────┐
trigger │  call components  →  build element tree  →  reconcile/diff  │
(state, └────────────────────────────┬────────────────────────────────┘
 props,                              │  (pure, no DOM, can be discarded)
 context)                           ▼
        ┌─────────────────────── COMMIT PHASE ───────────────────────┐
        │  mutate DOM  →  update refs  →  run layout effects  →  paint │
        └────────────────────────────┬────────────────────────────────┘

                            run passive effects (useEffect)

The takeaway: a render does not imply a DOM write. If the new tree is identical to the old one for a given node, React renders (calls the function) but commits nothing for that node. This is why you can see your component log on every keystroke while the browser’s paint flame chart stays empty — the work was thrown away during reconciliation.

Rendering is not updating the DOM

Conflating the two leads to bad mental models and premature optimization. Re-rendering a thousand components that produce the same output costs some JavaScript CPU time but zero DOM work. Conversely, a single component can cause an expensive layout thrash if it commits a large DOM change. Optimize the phase that is actually slow: use the React DevTools Profiler to see render counts and commit durations rather than guessing.

Best practices

  • Treat render functions as pure: no fetches, subscriptions, timers, or DOM mutations during render — put those in effects.
  • Expect children to re-render when a parent does; reach for React.memo only when a profiler shows an expensive subtree, not preemptively.
  • Compute derived values during render instead of storing them in state and syncing with effects.
  • Use useLayoutEffect only when you must read or write layout before paint; prefer useEffect otherwise to avoid blocking the browser.
  • Don’t call state setters with unchanged values expecting an update — React bails out on Object.is-equal values.
  • Profile commit duration, not just render counts; a frequent render with no commit is usually harmless.
Last updated June 14, 2026
Was this helpful?