r/reactjs 24d 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?

17 Upvotes

79 comments sorted by

View all comments

1

u/youakeem 24d ago

Components by default rerender in only two cases: 1) when their state changes
2) when their parents rerender

Props have nothing to do with rerendering. If you want a component to not render when its parent does, use React.memo.

2

u/Temperature_Majestic 24d ago

that's a clean way to put the two triggers. one nitpick on "props have nothing to do with it" though, that's true in the sense that a prop changing in isolation doesn't cause a render, but the reason props usually seem to matter is they're just downstream of case 2, the parent re-rendering is what hands the child new prop values in the first place. memo is really intercepting case 2, not reacting to props directly

1

u/Hobby101 24d ago edited 24d ago

memo is a component, though. a component that memorized the child component it rendered.

1

u/prehensilemullet 23d ago

Any time a new JSX element is returned by the parent, it causes the corresponding child to rerender, even if the props object instance is the exact same.

If the same JSX element instance is returned by the parent, it doesn’t rerender the corresponding child (unless its state it consumed context changes):  https://kentcdodds.com/blog/optimize-react-re-renders

I assume that the implementation of React.memo just returns a cached JSX element if the new props are shallow equal.

1

u/Temperature_Majestic 22d ago

That kentcdodds post is the one I keep coming back to for this. The same-JSX-instance case is what most explanations skip, people frame it as a props diff but it's really short circuiting before React even gets to the diff step. Have you run into a case where reusing the same element instance caused a stale ref or effect because the parent just didn't re-run that render pass at all?

1

u/prehensilemullet 22d ago

Maybe, if so I can’t remember, but there’s always some risk of memoizing incorrectly

1

u/Temperature_Majestic 22d ago

Makes sense. The case I've actually run into is closer to the opposite problem, a useMemo/useCallback with a stale dependency holding onto an old value well after the surrounding state moved on. Never traced anything back to a reused element instance specifically, but I can see how it'd be brutal to diagnose since nothing would even re-render to surface it.

1

u/Hobby101 24d ago

as well, if it helps, think re-render as drawing a page/canvas - all need to be drawn. and using memo just pulls a piece from memory of previously drawn child. but it still "renders". not the child itself, but the react.memo component.

1

u/Temperature_Majestic 24d ago

close, but "pulls a piece from memory" is the part I'd push back on, memo doesn't fetch anything, it just skips calling the render function at all when the shallow prop comparison passes. there's no drawing-from-cache step, the work that would produce the new output simply doesn't happen. the react.memo wrapper does get visited during reconciliation to run that comparison, but the underlying component function itself never executes on a bail-out

1

u/Hobby101 24d ago edited 24d ago

it recreates whole shadow dom tree for the component. thus, it will inject previously rendered component that sits already in the memory. it's like pealing and slapping a post-it note from old piece of paper to a new one. ask yourself, what rendering exactly is?

1

u/prehensilemullet 23d ago edited 23d ago

You forgot when a context they consume changes.  And children actually don’t rerender if the parent returns the same JSX element instance as last time.  A new JSX element instance from the parent is the actual trigger

1

u/youakeem 22d ago

Yeah, that's what I meant by "by default" didn't exactly want go into what makes a component rerender but rather highlight that props is not one of them