DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

Why components passed as children don't re-render with the parent (a free re-render boundary in React)

Here's a React behaviour that looks like magic until you know the rule behind it: a component passed as children doesn't re-render when its parent re-renders. Not because of memo, not because of anything you did — just because of who created the element. I built a demo with live render counters that makes it obvious.

▶ Live demo: https://children-rerender.pages.dev/
Source: https://github.com/dev48v/children-rerender

Same child, two placements

Case 1 — the wrapper creates the child:

function Wrapper() {
  const [n, setN] = useState(0);
  return (
    <div onClick={() => setN(n + 1)}>
      <Child />        // created HERE
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Click to bump n, and the <Child> render counter climbs right along with the wrapper. Every time Wrapper renders, it creates a new <Child /> element, so React re-renders the child.

Case 2 — the child is passed as children:

function Wrapper({ children }) {
  const [n, setN] = useState(0);
  return (
    <div onClick={() => setN(n + 1)}>
      {children}       // created by the PARENT
    </div>
  );
}

<Wrapper><Child /></Wrapper>   // App creates <Child/>
Enter fullscreen mode Exit fullscreen mode

Now bump the wrapper all you want — the child counter doesn't move. In the demo, one wrapper's child climbs to 3 while the other's stays at 1.

The rule

A component re-renders when:

  1. its own state or props change, or
  2. its parent re-renders and hands it a newly-created element.

That second clause is the whole thing. <Child /> is just React.createElement(Child, ...) — it produces a plain element object. When Wrapper owns that JSX, it makes a fresh element every render, and React reconciles it (re-renders the child). But when Child is passed in as children, the element was created once by App, up the tree. App isn't re-rendering, so the children prop Wrapper receives is the same object reference across all of Wrapper's re-renders. React compares, sees identical element, and bails out of re-rendering that subtree.

No memo. No useMemo. It's structural — a consequence of where the element is created.

Why you'd exploit this

It's a free re-render boundary. The classic case: a component with fast-changing state that wraps something expensive.

// 🐞 MousePosition re-renders <ExpensiveTree/> on every mousemove
function MousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(/* track mouse → setPos */);
  return <div>{pos.x},{pos.y} <ExpensiveTree /></div>;
}

// ✅ pass the expensive part as children → it never re-renders on mousemove
function MousePosition({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(/* track mouse → setPos */);
  return <div>{pos.x},{pos.y} {children}</div>;
}
<MousePosition><ExpensiveTree /></MousePosition>
Enter fullscreen mode Exit fullscreen mode

This is exactly why <Context.Provider value={...}>{children}</Context.Provider> doesn't re-render your whole app when the provider re-renders — the children come from above. It's also the cleanest fix for "my provider/layout/animation wrapper is re-rendering everything": lift the expensive JSX up and pass it down.

Caveat: it only helps when the children genuinely don't depend on the changing state. If they need pos, they have to re-render — that's correctness, not waste. And it's the element that has to stay stable, not just the component type.

Bump both wrappers in the demo and watch one child freeze at 1. If it made the children/re-render rule click, a star helps others find it: https://github.com/dev48v/children-rerender

Top comments (0)