skip to content

In React, you move `<ExpensiveTree />` out of a stateful `<Counter>` component and instead pass it into `<Counter>` as `children` from the parent. Why does `ExpensiveTree` stop re-rendering when the counter's state changes, even though nothing is wrapped in React.memo?

level: middleimportance: should knowfreq 55%

answer

  1. who writes the JSX matters
  2. children is just a prop
  3. same element object, not a new one
  4. props unchanged means React bails out
  5. boundary moved, nothing cached

basics

~20 s

The children element is created in the parent's render, not inside the stateful component. When only that component's state changes, its children prop is the same object as the previous render, so React bails out and reuses that subtree.

solid answer

~50 s

JSX is data: `<ExpensiveTree />` compiles to a call that returns a plain element object, and that object is created by whoever writes the JSX. When the tree lives inside `Counter`, every `Counter` render creates a brand-new element, so the subtree renders again. When the parent writes it and passes it down, the element is created in the parent's render. A `setCount` update re-renders only `Counter`, so `props.children` is the identical object it was last time. React sees an element whose type and props are unchanged for that position and bails out, keeping the existing subtree untouched. No cache, no equality function, no `React.memo` — the work simply never gets scheduled. The catch is that this only holds while the outer parent itself does not re-render; if it does, it creates a fresh children element and the subtree renders again.

code

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

function ExpensiveTree() {
  console.log('ExpensiveTree rendered');
  return <p>heavy</p>;
}

function Counter({ children }) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {children}
    </div>
  );
}

export default function App() {
  return (
    <Counter>
      <ExpensiveTree />
    </Counter>
  );
}

go deeper

for a junior

Know that JSX produces plain element objects and that children is just a prop. Be able to say that the component receiving children did not create that content, so it cannot force it to render again.

for a middle

Explain the mechanism out loud: the element is authored in the parent, a state update re-renders only the state's owner, the relayed prop is the same object, and React bails out on unchanged props. Name the limit — the parent re-rendering ends it.

for a senior

Show that you reach for this before memoization on real code: refactor a stateful wrapper to accept children, and explain that it removes work permanently rather than caching a comparison that a future inline prop can break.

for a principal

Own it as an API convention. Wrappers that hold state should take content as props by default, so teams get the cheap render boundary without knowing why, and memo boundaries stay rare, named and justified.

## The two shapes being compared The question contrasts two arrangements of exactly the same UI. ```jsx // A: the expensive tree is written inside the stateful component function Counter() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> <ExpensiveTree /> </div> ); } // B: the expensive tree is written by the parent and passed in function App() { return ( <Counter> <ExpensiveTree /> </Counter> ); } function Counter({ children }) { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> {children} </div> ); } ``` In shape A, clicking the button re-renders `ExpensiveTree`. In shape B it does not. Nothing was memoized; only the authorship of one element moved. ## JSX is data, and its author matters With the automatic JSX transform, `<ExpensiveTree />` compiles to a call into `react/jsx-runtime` that returns a plain object describing what to render: a type, a props object, a key. It does not call the component. Creating the element is cheap; rendering it is what costs. That object is produced by the function whose body contains the JSX. In shape A, `Counter`'s body contains it, so a new element object is produced on every `Counter` render. In shape B, `App`'s body contains it, so the element is produced once per `App` render and then handed to `Counter` as a prop named `children` — `children` is an ordinary prop that JSX fills in from the nesting syntax, nothing more. ## Why the re-render stops When `setCount` runs, React marks `Counter` as needing work. It re-renders `Counter`, which returns new elements for the parts `Counter` itself writes (the `div`, the `button`, the text) and passes `props.children` straight through, untouched. That value is the *same object reference* as the previous render, because nobody re-created it. React then reconciles the returned children against the previous tree. For the position holding the expensive subtree, the incoming element has the same type and, crucially, the identical props object as before. With no pending state update inside that subtree and no context it reads having changed, React bails out: it keeps the existing fiber and its rendered output and never calls `ExpensiveTree`. The useful way to say this in an interview: *the render boundary moved up*. `Counter`'s state can only re-render `Counter` and the elements `Counter` itself creates. Content it merely relays is out of reach. ## The limits — this is structure, not a cache - **If the parent re-renders, the trick stops paying.** `App` re-rendering creates a fresh `<ExpensiveTree />` element, so the subtree renders again. Composition relocates the boundary; it does not remember anything. - **Context still gets through.** A bailed-out subtree still re-renders components inside it that read a context whose value changed. Reference identity does not shield consumers. - **Closures in the passed JSX.** If the parent's JSX captures values that change, the parent re-renders anyway and the identity argument is moot. - **Own state and props of the subtree win.** A component in the passed subtree that sets its own state still re-renders normally. ## Why it is preferred over `React.memo` here `React.memo` solves the same symptom with a runtime cache: it keeps the last props, shallow-compares them on every parent render, and skips work when they match. That comparison costs something on each render, it retains the previous result, and it silently stops working the moment a prop is an inline object or arrow function. The composition version has none of that machinery — there is no comparison to get wrong and no precondition to keep valid as the code evolves. That durability is why interviewers phrase this as "composition over memoization". ## Where you actually see it Any wrapper that owns local state and renders content it did not author: layout shells with a collapsible sidebar, tab and accordion containers, modal hosts, drag-and-drop wrappers, and provider components that hold state and render `{children}`. Writing such a component to take `children` rather than importing and rendering the content itself is a one-line design choice that removes a whole class of re-render cost before it exists.

  • Does the same argument hold for a subtree passed through a differently named prop, such as `header={<Toolbar />}`?
    Yes. `children` is an ordinary prop that the JSX nesting syntax happens to fill in, and React gives its identity no special treatment. Any element-valued prop created in a parent that is not re-rendering arrives unchanged and gets the same bailout. Named element props are just a readability choice when a component relays several slots.
  • If the outer parent does re-render, has the restructuring bought you nothing?
    It stops helping for that update, but nothing is worse than before — you paid no runtime cost for the arrangement. At that point you either move the boundary again, so the state that keeps waking the parent lives lower, or you add a memo boundary at the subtree root. Measure before assuming the second is needed.
  • Does a bailed-out subtree still react to context changes?
    Yes. Element identity only lets React skip re-rendering components whose props and state are unchanged; components inside that subtree which read a context are subscribed independently, and React re-renders them when that context's value changes. Composition never insulates consumers from their providers.

Handing someone a sealed envelope to carry: they can walk around all day, but the letter inside is never rewritten. Only the person who wrote it can produce a new one.

saying these in an interview costs you the question

  • Says any component nested inside a re-rendering component must re-render
  • Claims React.memo is required for any subtree to skip re-rendering
  • Thinks passing children clones or deep-copies the subtree each render
  • Believes children are rendered lazily, only when the child asks for them
  • Assumes the trick keeps working when the outer parent re-renders too

context