skip to content

During a React re-render, under what conditions does React skip re-rendering a child subtree even though no React.memo is involved, and why does that almost never happen for a child written inline in the parent's JSX?

level: seniorimportance: should knowfreq 36%

answer

  1. not shallow equality
  2. the same object, not an equal one
  3. who created the element, and did they re-render
  4. three conditions, all required
  5. pending work still gets visited

basics

~20 s

React skips a child when its props object is referentially identical to the previous render, no update is pending on it, and no context it reads has changed. Inline JSX builds a new props object every render, so this rarely fires.

solid answer

~50 s

React can bail out at a fiber when three things hold: the new props object is the *same object* as the previous one, no state update is queued on that fiber, and no context it consumes has a new value. In that case React does not call the component function at all — it reuses the previous output and clones the existing children. The reason this rarely helps is that JSX creates a fresh element with a fresh props object on every render of the parent, so a child written inline never satisfies the reference check. Where it does fire is an element created *above* and passed down unchanged — the classic `{children}` pass-through — because a re-render of the middle component does not recreate that element. React still descends into any subtree that has its own pending work, so a bailed-out parent never blocks a child's own state update.

code

jsx · 23 lines
jsx
import { useState } from 'react';

function Report() {
  return <p>expensive report</p>;
}

function Shell({ children }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>{open ? 'hide' : 'show'}</button>
      {children}
    </div>
  );
}

export function Page() {
  return (
    <Shell>
      <Report />
    </Shell>
  );
}

go deeper

for a junior

Know the default: when a component re-renders, the children it renders re-render too. Skipping that is the exception, not the rule.

for a middle

Explain why the default holds — JSX builds a new props object each render, and React's built-in check is reference identity on that object, not a comparison of the values inside it.

for a senior

Reason about where the bailout genuinely fires: elements created by a component that did not re-render, such as a children pass-through, and be clear that React still descends into subtrees carrying their own pending work.

for a principal

Own the fragility angle: a bailout that depends on where an element happens to be created can vanish in a refactor with no error and no test failure, so treat it as a property of tree shape to be stated explicitly, not as an optimisation to rely on silently.

## What the bailout actually tests When React reaches a fiber during the render pass, it does not automatically call the component. It first asks whether anything about this position could have changed. Three conditions must all hold for it to skip the work: 1. **The props object is referentially identical** to the one from the previous render. Not shallow-equal — the same object. This is a pointer comparison, not a per-key comparison. 2. **No update is queued on this fiber.** If the component called its own setter, it has work of its own and must render. 3. **No context it subscribes to has a new value.** A context update reaches consumers regardless of props. When all three hold, React reuses the previous rendered output for that position and clones the existing child fibers rather than reconciling fresh ones. The component function is never invoked, its hooks are not re-run, and nothing below it is re-rendered — unless a descendant has its own pending work, which React tracks separately and descends into. That last point matters: a bailout is not a barrier. If a component deep in a skipped subtree scheduled a state update, React walks down to it, cloning the fibers on the way, and renders just that component. A parent skipping work never strands a child's update. ## Why an inline child almost never qualifies JSX compiles to element creation, and element creation builds a new props object: ```jsx function Parent() { const [n, setN] = useState(0); // a new element AND a new props object on every render of Parent return <Child label="hi" onPick={handlePick} />; } ``` Even with literally identical prop values, `Parent` re-rendering produces a props object that is not the previous object. Condition one fails, so `Child` renders. This is the default React behaviour everyone knows — a parent re-render re-renders its children — and this reference check is the reason for it. The props are equal in value; they are not the same object, and React does not compare values here. Comparing values is exactly what `React.memo` opts into, which is a separate mechanism. ## Where it does fire The element has to be created somewhere that did not re-render. The common shape is an element created by an outer component and handed through as a prop: ```jsx function Shell({ children }) { const [open, setOpen] = useState(false); return ( <div> <button onClick={() => setOpen(!open)}>toggle</button> {children} </div> ); } function Page() { return <Shell><Report /></Shell>; } ``` When `Shell`'s own state changes, `Page` does not re-render, so the `<Report />` element object is not recreated. `Shell` renders and places the exact same element it received into the same position. Its props object is identical to last time, nothing is queued on it, so React bails out and `Report` never renders. The same holds for any element-valued prop (`header={<Header />}`, `icon={<Icon />}`) and for a stable element captured outside the render path. What all these share is that the *creator* of the element did not re-render. ## Reading the mechanism correctly Two misreadings are common. The first is thinking React shallow-compares props by default; it does not, and the difference between reference identity and shallow equality is precisely the line between the built-in bailout and `React.memo`. The second is treating a bailout as a guarantee, when it is a consequence of the tree's shape: change where an element is created and the bailout disappears, silently, with no error. It also helps to be precise about what a bailout saves. It skips calling the component and reconciling its output. It does not skip the commit for work elsewhere, and it does not make an expensive render cheaper — it avoids one that was unnecessary. And it applies per position, not per component: the same component type can bail out at one position and render at another in the same pass. ## Talking about it in an interview A strong answer names the three conditions, states that the props check is reference identity rather than shallow equality, and then explains the `{children}` case as the natural consequence rather than as a trick. Mentioning that React still descends into subtrees with pending work shows you understand that bailouts are an optimisation inside a correct algorithm, not a way of blocking updates.

  • How is this bailout different from what React.memo does?
    The built-in bailout tests whether the props object is the same object; React.memo opts into a shallow per-key comparison of a props object that is genuinely new. Memo is what you reach for when the parent re-renders and recreates the element but the values are unchanged. The built-in check never runs a per-key comparison.
  • If React skips a component, can a state update inside its subtree still be processed?
    Yes. React tracks that a descendant has pending work and walks down to it, cloning the fibers it passes through and rendering only the component that scheduled the update. A bailout at one level never strands work below it — that would make updates unreliable rather than merely slower.
  • Does a context update reach a component whose props object was identical?
    Yes — a changed context value is one of the three conditions the bailout requires to be absent. A component reading that context is re-rendered even when its props object is unchanged and nothing is queued on it, because context is a second input to its output.
  • Can the same component bail out at one position and render at another in the same pass?
    Yes. The check is per fiber, so it is really per position in the tree. One instance may receive an identical props object while a sibling of the same type receives a freshly created one, and only the second is rendered.

saying these in an interview costs you the question

  • Says React shallow-compares props before re-rendering children
  • Confuses the built-in bailout with React.memo's comparison
  • Thinks a bailed-out parent blocks updates in its subtree
  • Claims equal prop values are enough to skip the render
  • Believes context changes cannot reach a skipped component

context