Command Palette

Search for a command to run...

Hectal
PHASE 16Advanced ~22 min· topic 1 of 10

Topic 16.1

Why Components Re-render

In one line

A component renders again for three reasons only: its own state changed, its parent rendered, or a context it reads changed; rendering means calling your function, which is cheap, and React touches the DOM only where the result is different.

Think of it like this

A teacher asks the class "is anything different in your homework since yesterday?". Every child checks their own notebook (that's rendering), but only the child whose answer changed walks to the board and rewrites it (that's updating the DOM). Asking is quick. Rewriting the board is the slow part, and React only does it where something really changed.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Render
React calling your component function to get a description of the screen. No DOM is touched yet.
Re-render
Any render after the first one, caused by a state, parent or context change.
Commit
React applying the differences it found to the real DOM.
Subtree
A component and everything inside it: its children, their children, and so on.
Object.is
The comparison React uses to decide whether a state or context value changed. Like ===, with small differences for NaN and -0.
Bail out
React deciding it can skip rendering a component (or its children) because nothing it depends on changed.
Cost model
A rough picture of what an update costs: JavaScript for rendering, then DOM work for the parts that changed.

Step by step

01Find who owns the state

When something on screen changes, first ask: which component called useState for this value? That component, and everything below it, is what renders. Everything above it, and its siblings' subtrees, stay as they are.

Find who owns the statediagram
Rendering diagram…

02Parents render their children

App returns <Title />. When App renders, React creates a new Title element and calls Title again, even though Title takes no props at all. Nothing is wrong: Title returns the same <h2>, React compares it and changes nothing in the DOM.

App.jsxwhole filejsx
function App() {
  const [count, setCount] = useState(0);
  return (
    <main>
      <Title />                {/* renders whenever App does */}
      <Display count={count} />
    </main>
  );
}

03Setting the same value bails out

setCount(count) with the current value doesn't render the children. React may call App one extra time to be sure, throw that result away, and stop. Don't rely on the exact number of these extra calls: rely on components being pure (Topic 2.6), so extra calls are harmless.

04Context skips the middle

A context reader renders when the context value changes, even if its parent was skipped with memo. Context is a direct line from the provider to every reader.

Context skips the middlediagram
Rendering diagram…

05Rendering is not DOM work

After rendering, React compares results. If a <p> produces the same text and attributes, its DOM node isn't touched at all. You can prove it with a MutationObserver, a browser API that reports every DOM change, as the second live example does.

06Decide if it matters

Extra renders only matter when they cost time the user notices. Measure first (Topic 16.6). Usually the fix is structural: move state down to where it's used, or pass slow parts as children so they aren't re-created. memo and friends come after that.

Try it yourself

  1. 1

    Move the state down

    In "The three reasons, logged", predict what happens if Display owned the count itself (move useState and the Add button into Display). Which lines print on each click? Make the change and check: App and Title stop rendering.

  2. 2

    Remove the memo

    In the context example, replace memo(function Toolbar() {...}) with a plain function Toolbar(). Predict the log for "change other state". Now Toolbar and ThemedButton render on every App render, yet the DOM count stays 0.

  3. 3

    Pass the slow part as children

    Write a Layout({ children }) that owns its own useState counter and renders {children} below its button. Render <Layout><Title /></Layout> from App. Click the Layout's button: does Title render? (No. Title's element was created by App, which didn't render, so React sees the same element object and skips it.)

Code & diagrams

The three reasons, logged New tab

Add one: App and both children render (reason 1, then reason 2). The first "same value" click calls App once more and throws the result away, without rendering the children. The second is skipped entirely.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

App rendered
  Title rendered
  Display rendered, count = 0
--- click Add one
App rendered
  Title rendered
  Display rendered, count = 1
--- click Set the same value
App rendered
--- click Set the same value again
Context reaches past memo, and rendering isn't DOM work React 19+ New tab

Toggling the theme skips Toolbar (memo, no props changed) but ThemedButton still renders: it reads the context. App rendered three times, yet the static <p> was never touched in the DOM. `<ThemeContext value>` as a provider is React 19 syntax (Topic 9.2).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

App rendered
  Toolbar rendered
    ThemedButton rendered with light
--- toggle theme
App rendered
    ThemedButton rendered with dark
--- change other state
App rendered
DOM changes inside the static <p>: 0
What one state update costsdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Expect a ref change to update the screen

Store the count in const count = useRef(0) and do count.current++ in the click handler, showing {count.current} in the JSX.

terminal
$ Preview
── what you'll see ──
Count: 0
(clicking does nothing on screen, no render is logged)

Myth vs fact

Myth

A component re-renders when its props change.

Fact

It re-renders when its parent renders, whether or not the props changed. Comparing props is opt-in with memo.

Myth

Every re-render rewrites the DOM.

Fact

Rendering only calls your function. React changes a DOM node only when the new output for it differs. A thousand renders with the same output cost zero DOM work.

Myth

Re-renders are bad and should be avoided.

Fact

Re-renders are how React works. They matter only when they are slow enough for the user to notice. Measure before optimising.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The "same value" bail-out has two paths. If the component's fiber has no pending work, React compares the new state eagerly inside setState and doesn't even schedule a render. Otherwise it must render the component to compute the state and bails out of the children only after that render: the extra App rendered line you saw.

  • ▸

    The cheapest optimisation is composition. Elements passed in as children (or any prop) were created by the owner component, so when an intermediate component re-renders with its own state, those elements are the same objects as before and React skips them without memo.

  • ▸

    Context has no selectors: a reader re-renders on any change to the value, even if it only uses one field. Splitting contexts, memoising the value, or moving to an external store with selectors (useSyncExternalStore, Zustand, Topic 15.2) are the scaling answers.

  • ▸

    In development, StrictMode calls every component twice per render (Topic 7.4), and the development build does extra checks. Never judge real speed from a development build: profile a production build.

Remember this

  1. 1

    Rendering means React calling your component function to get a fresh description of the screen (React elements, Topic 4.8). A re-render is any render after the first one. Re-rendering is not a bug: it's how React finds out what the screen should look like now. A performance problem only exists when renders are so many, or so expensive, that the user feels it: typing lags, clicks feel slow, scrolling stutters.

  2. 2

    Reason 1: state changed. Calling a state setter (setCount(1)) or dispatch schedules a render of the component that owns that state. React compares the new value with the old one using Object.is. If they are the same (setCount(count)), React usually skips the render entirely. One known detail: right after an update, React may still call the component once more before it can prove nothing changed. It throws that result away and does not render the children. The live example below shows exactly this.

  3. 3

    Reason 2: the parent rendered. When a component renders, React renders every child it returns, all the way down, whether or not their props changed. React doesn't check props by default, because comparing props costs time too and most components are cheap. This is the biggest source of "extra" renders, and it's the default you opt out of with memo (Topic 16.3).

  4. 4

    Reason 3: a context changed. A component that reads a context with useContext(SomeContext) or use(SomeContext) renders again whenever the provider's value changes (compared with Object.is), even if every parent in between skipped rendering. Context updates go straight to the readers. That's why a provider whose value={{ user, setUser }} is a new object on every render makes every reader render every time (Topic 9.3).

  5. 5

    Props changing is not a reason by itself. Props only change because a parent rendered and passed new ones (reason 2). And a component never re-renders because an ordinary variable, a ref (ref.current, Phase 8) or a global object changed: React doesn't watch those. If the screen must change, the data has to go through state, props or context.

  6. 6

    The cost model. A state update costs: (1) render time: calling every component in the affected subtree, which is plain JavaScript; (2) reconciliation: comparing the new elements with the old ones (Topic 16.2); (3) commit: changing the DOM nodes that differ; then the browser's layout and paint. Steps 3 and later are the expensive ones, and React keeps them as small as possible. So a component that renders often but produces the same output costs some JavaScript time and zero DOM work. Optimise only when steps 1–2 become large: a big subtree, a slow calculation, or thousands of list items.

Explain it without notes

01

What are the three reasons a React component re-renders?

02

Why doesn't a re-render always mean a DOM update?

03

A component reads a context and its parent is wrapped in memo. The context value changes. What happens?

04

Why does React re-render children even when their props are unchanged?

Practice

01

A Clock component in the header owns a time state that updates every second, and the header also renders a slow Menu. Restructure it so Menu doesn't re-render every second, without memo.

02

Predict the logs when a button calls setName("Asha") and name is already "Asha", in a component that has never updated before.

Trade-offs

  • ↔

    Rendering whole subtrees by default keeps code simple and always correct, at the price of extra JavaScript work that occasionally needs optimising.

  • ↔

    Moving state down makes renders cheaper but can make state harder to share; lift it up again only as far as it needs to go (Topic 6.3).

Done when you can

  • I can name the three reasons a component re-renders.

  • I can explain the difference between rendering and committing to the DOM.

  • I can predict which components render after a state change.

  • I know why setting the same state value usually skips rendering.

  • I can reduce renders by moving state down or passing children.