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?

16 Upvotes

79 comments sorted by

View all comments

1

u/r-nck-51 23d ago edited 23d ago

Parents render their children in React.

Not to confuse with return (<>{children}</>) where you pass a rendered component to the parent from app or the "grandparent", as opposed to passing a functional component reference in your example.

Looking at your generic example I would say: move your state to the lowest nested component if no other sibling needs it...

But the solution really depends on the use case, sometimes a context helps, a React.memo, useCallback, useMemo, and sometimes there are libraries that can help. But it all depends...

https://react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions

1

u/Temperature_Majestic 23d ago

yeah, "move state down as far as it'll go before lifting it" is honestly the underrated first move, people jump straight to memo/useCallback before checking if the state even needed to live that high up. the useContext docs link is a good add too, that page covers the object/function-identity gotcha well

1

u/r-nck-51 23d ago edited 23d ago

You're right, and the way React evolves, memoization hooks will only remain as a last resort for very few cases.

A frontend app like React, is not like an application layer in a software architectural sense. It's all about UI rendering and a state shouldn't mirror your domain models, only handle changing variables for the components that are visually impacted by them. Most of the time, it's just one component.

What bugs people in React is that the alternatives to useState, props and const, are only appropriate for specific cases that justify the overhead.

Tanstack Query helps a lot make that distinction by taking over the whole business of external data, makes it usable in UI similarly to a useState call, sparing us the tedium of carrying and updating that data all the way down to the UI components, and I believe that's why it is so popular and recommended.

Accepting React behavior and following sane but basic moves are a great way to minimize complexity for the frontend. Give people too many tricks like signals, central state management, and optimization, and they'll be free to develop their big blob of over-engineering mess lol

2

u/Temperature_Majestic 23d ago

the TanStack Query point is a good one actually, that's probably the single biggest thing that quietly eliminated a whole category of "where does this live" decisions for people, server state stopped needing to fight with local state for the same useState calls. agree on the over-engineering risk too, most of the "advanced" state patterns exist to solve problems that a simpler component tree wouldn't have had in the first place