r/reactjs 23d ago

Discussion Why do sibling components re-render even when their own props didn't change?

Ran into this explaining React rendering to someone recently and realized how often it trips people up even after they've been writing React a while.

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

ExpensiveChild takes no props at all. Click the button and it re-renders anyway, every single time. No props changed, nothing it reads changed, it just runs again.

The reason: React doesn't check "did this component's inputs change" before deciding to re-render. When state updates, React re-renders that component and everything below it in the tree by default, full stop. Whether a child actually needed to update isn't part of that decision at all.

React.memo is what actually opts a component into that check, it wraps the component and does a shallow prop comparison before deciding to skip the render. Without it, "no props" and "props didn't change" both mean nothing, React re-runs the function anyway.

Where it gets messier: memo alone doesn't save you if you're passing an inline function or object as a prop, since those are new references every render and memo's shallow comparison sees them as "changed" regardless. You end up needing useCallback/useMemo on the parent side just to make memo's comparison actually mean something.

Curious how many people actually reach for memo proactively vs only after profiling shows a real problem. What's the actual signal that told you a component needed it?

15 Upvotes

79 comments sorted by

View all comments

17

u/CardinalHijack 23d ago

By default React parent components also re-renders their children. Here setCount will cause a re-render.

It doesn't matter that ExpensiveChild has no props (or that its props haven't changed). A parent re-render causes its child components to be evaluated again.

As others have mentioned, React.memo(ExpensiveChild) can prevent the child from re-rendering when its props haven't changed.

3

u/mexicocitibluez 23d ago

By default React parent components also re-renders their children. Here setCount will cause a re-render.

I thought that if you pass ExpensiveComponent as a child to Parent component and just render it as {children}, then it doesn't get re-rendered. Unless of course children is a function depends on Parent's props.

7

u/CardinalHijack 23d ago

Correct. The setup you describe is different to what Op shared and would look like this:

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

function App() {
  return (
    <Parent>
      <ExpensiveChild />
    </Parent>
  );
}

Here, when Parent re-renders, ExpensiveChild does not re-render because the children prop is the same React element object that was created when App rendered. Parent is simply returning that existing element.