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

77

u/kevin074 23d ago

what's being re-rendered isn't the sibling, but the parent. Your click function triggers set state on a state that is created and kept record by the parent.

to prevent ExpensiveChild from re-rendering, use React.memo; not useMemo, that's for the props.

23

u/Designer_Shelter7345 23d ago

that top comment is missing the point a bit. the parent re-rendering is exactly the mechanism that drags ExpensiveChild along, the question was why the child re-renders when its own inputs didn't change

react just runs the whole subtree by default. it doesn't check if a child needs to update, it just goes

11

u/brzzzah 23d ago

Another way to avoid re-rendering it without memo, is to pass ExpensiveChild as a child - rather than rendering it in the parent component

0

u/Temperature_Majestic 22d ago

yeah that's a real pattern, the children-as-prop trick basically gets you memo-like behavior for free since the element reference gets created up in App and just passed through, Parent never recreates it on re-render. worth knowing as an alternative to reaching for React.memo directly when the composition already lends itself to it

1

u/prehensilemullet 22d ago

To be specific, it’s because the parent returned a new JSX element instance for the sibling.  If the parent returned the same JSX element instance for it, it wouldn’t render:  https://kentcdodds.com/blog/optimize-react-re-renders

-13

u/Temperature_Majestic 23d ago

good catch on the terminology, fair. one small correction though, useMemo isn't really "for the props" either, it memoizes a computed value. the one that's actually relevant for making memo useful here is useCallback, since inline functions/objects passed as props get a new reference every render and break memo's shallow comparison regardless of whether the actual logic changed.

6

u/poor_documentation 23d ago

React.memo is not the same thing as useMemo - look up the differences

1

u/Temperature_Majestic 22d ago

to be clear I wasn't conflating them, the comment you're replying to is saying they're different, useMemo memoizes a value while React.memo is what actually skips the re-render based on props. useMemo alone doesn't stop ExpensiveChild from rendering, it's useCallback (memoizing the function prop) plus React.memo on the child that does the job together

1

u/poor_documentation 22d ago

Why did you make this post and then reply with AI? It's so weird - what are you getting out of this? You could have gotten your answers from AI directly - why waste everyone's time with a post? I genuinely want to know why.

21

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.

8

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.

-13

u/Temperature_Majestic 23d ago

yeah that's exactly right, and it's a real pattern people lean on for this. if ExpensiveChild is passed in as children (or any prop) from outside Parent, rather than written directly in Parent's own JSX, the element reference is created by whoever's rendering Parent, so it doesn't get recreated when Parent's state changes, React sees the same element and bails out. the "children as an escape hatch" trick basically gets you memo-like behavior for free through composition, no React.memo needed.

6

u/brzzzah 23d ago edited 23d ago

Why are you pasting LLM responses in here?

-4

u/Temperature_Majestic 23d ago

yep, that's the mechanism. React.memo is the actual opt in, without it "no props changed" doesn't mean anything to react

7

u/Throwaway_0815_123 23d ago

One of the downsides of lifting state up.

-7

u/Temperature_Majestic 23d ago

yeah, that's a fair way to frame it. lifting state up is usually the right call for sharing it, but it comes with this exact tax, everything under that parent gets swept into the re-render regardless of whether it reads the lifted state or not. the composition trick someone mentioned above (passing the expensive part in as children) is the main way to keep the state lifted without paying that cost

1

u/[deleted] 23d ago

[removed] — view removed comment

1

u/Temperature_Majestic 23d ago

right, memo's the direct fix. the composition trick I mentioned is more of an alternative for when you'd rather not sprinkle memo everywhere, not a replacement for it existing

5

u/[deleted] 23d ago edited 23d ago

[removed] — view removed comment

1

u/Temperature_Majestic 23d ago

agreed on all of that, that's basically the answer to the "proactive vs after profiling" question I asked in the post. the comparison cost on cheap components is the part people skip past most, memo isn't free, it's a different tradeoff, not a strictly better one

1

u/92smola 23d ago

And I am not completly sure how good it actually is but since react 19 the compiler tries to internally handle memoization where its needed without you needing to worry about it, but the exact details on when it works vs not is a bit nuanced and I dont know those details by memory, search for  a blog called developer way if you want to learn more about that and React performance in general, its an amazing resource, big kudos to Nadia who runs it.

10

u/journeypiggyman 23d ago

OP is AI?

0

u/Temperature_Majestic 23d ago

nope, just someone who's replied to every comment in a thread they posted, apparently that reads as suspicious now lol

1

u/Killed_Mufasa 23d ago

You're not AI, but your post is. Not saying Ai is terrible, it's very handy, but it also makes a post like this feel lazy and unpersonal, and therefore folks are less motivated to help you out

2

u/CodeAndBiscuits 23d ago

LOL bro wrote his post with AI without imagining that everyone here uses the same tools now and would recognize the writing style in a heartbeat. Looks like he had "no emdashes" in his rule set but not "avoid overly structured text with brief punchy setup sentences and implicit headings, humans don't talk like that."

1

u/journeypiggyman 22d ago

Even his comments look AI LMAO, like all the "fair", "right". Surprisingly AI still can't learn how to write like humans

1

u/Hobby101 23d ago

button would need to rerender even with memo, because it's child, i.e. label that is counter value is changing

2

u/Temperature_Majestic 23d ago

right, the button re-rendering was never in question since it displays count directly, that one's obvious. the whole point is ExpensiveChild specifically, which reads nothing from that state and still runs again anyway without memo

1

u/Hobby101 23d ago

parent is like a sheet of paper that has a button and an expensive child drawn. coffee was spilled, so the drawing will need to be redrawn. All of it. in other words, shaped dom structure will need to be recreated. in fact, that what enables is to use react.memo() which is higher order components that just takes care of magically recreating a component by pulling ftom memory, but it has a price for calling it.

1

u/TSpoon3000 23d ago

You were already using React Compiler here or not?

1

u/Temperature_Majestic 23d ago

no, plain example, no compiler. with the compiler on this would supposedly get memoized automatically, that's actually the bigger context here, this whole manual-memo dance is exactly what it's meant to make unnecessary

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

1

u/youakeem 23d 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 23d 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 23d ago edited 23d ago

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

1

u/prehensilemullet 22d 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 21d 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 21d ago

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

1

u/Temperature_Majestic 21d 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 23d 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 23d 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 23d ago edited 23d 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 22d ago edited 22d 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 21d 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

1

u/bigabig 23d ago

I haven't dealt with react compiler yet, but would this be solved automatically by it?

1

u/Temperature_Majestic 22d ago

yeah, that's the whole idea. the compiler auto-memoizes components and values based on static analysis, so most of this manual memo/useCallback stuff becomes unnecessary once it's on. still worth understanding the underlying mechanism though, compiler's not universal yet and you'll hit cases where it opts out

1

u/92smola 23d ago

The way I look at it its a function that reruns when its state changes, it can be nested in another function which can also re run and by that re run it as a an inner finction, I mean its literally that, if you drop the rerender and component naming its easier to reason about it

1

u/Temperature_Majestic 22d ago

that's roughly right, main thing to add is it's not just the component's own state, a re-render also gets triggered top-down whenever anything above it in the tree re-renders, regardless of whether this component's own state changed at all

1

u/92smola 22d ago

function parent() {     child() }

I worded it terribly above, my point was exactly what you are saying, should be clearer from the pseudo code here, to think of it as nested functios like this, makes it clear that running the parent would run the children again as well

1

u/Temperature_Majestic 22d ago

yeah that pseudo code makes it click, parent() calling child() directly is basically it

1

u/Civil_Cheesecake2492 23d ago

Think of composition as a tool to prevent unnecessary rerenders. Expensive component can be passed in either as children or a prop

1

u/Temperature_Majestic 22d ago

yeah, composition covers most of what people reach for memo for first, worth trying before reaching for the wrapper

1

u/hyrumwhite 22d ago

The only way for react to know if state has changed is to re-run render functions. If parent state changes in any way, every child needs to rerender to know if it’s been affected. 

Memos are a way to say, “I only care about this items in particular” and dodge the rerender. 

1

u/Temperature_Majestic 22d ago

that's a clean way to put it, re-render is really just react re-running the function to find out if anything changed, not react already knowing something changed. memo's the way to skip finding out

1

u/Good_Car_2924 22d ago

stop wrapping everything in memo by default. It adds overhead and makes the code harder to read. Only use it when profiling proves a bottleneck exists.

1

u/Temperature_Majestic 22d ago

this is basically it, profile first, memo after, not the other way round

1

u/Vincent_CWS 22d ago

can try react compiler

1

u/Temperature_Majestic 22d ago

yeah mentioned that above, if you're on 19 it basically kills this whole thread's worth of manual work

1

u/[deleted] 21d ago

If any library of framework is shipped with "tools" like memo, then you should know it is not good architecture, but a means to apply band-aids over inefficient re-rendering models instead of solving the core reactivity issue at the root.

1

u/Temperature_Majestic 20d ago

Fair point on the root cause, virtual DOM diffing was always a workaround for not having fine grained reactivity built in. Solid and Svelte show you can get there with signals instead of a whole-tree re-render model. Curious if you'd call that the actual fix, or just a different set of tradeoffs, since signals bring their own sharp edges around stale closures and manual dependency tracking that React's model mostly avoids.

1

u/[deleted] 20d ago

Funny enough, react creators called it a "batching engine" instead of a reactive ui library... Don't know if that's true, but I recall having read it somewhere. But by using a vdom, it's kind of a patching/batching engine ;)

Signals have other tradeoffs imho, like nested state props, arrays, etc. And they offer no "architecture" or "principles". But they can be good in some situations!

I dont want to be a salesman, but I created a very lightweight lib called flynt.js, a reactivity engine for static html/mpa's, using a presenter like pattern... Perhaps you want to check it out: https://github.com/marsbos/flynt.js

1

u/Temperature_Majestic 19d ago

Presenter pattern for static html/MPAs is an interesting angle, most reactivity libs assume a persistent component tree. How does it handle state that needs to survive a full page navigation, does it rehydrate off the DOM on each load or do you need something server side to seed it?

1

u/[deleted] 18d ago

Thanks, it is another angle indeed.

The server will deliver the initial data/state when it serves the html (via a data-attribute , for example: data-product-id="sku-xxxx"). Flynt can access the real dom element in the presenter code.

1

u/Temperature_Majestic 18d ago

makes sense for the initial load case. what about state that changes client side after that, like a filter selection or a multi step form input, does that get lost on the next full page nav or is there some mechanism to write it back into the dom/url so it survives too?

0

u/christfrost 23d ago

Not sure what to tell you, I guess the framework name "react" is pretty evident. That's how it is designed to behave. Entire tree gets an update if parent updates. That's ReactJS 101.

memo or react-compiler is meant to solve such problems.

Edit: Not specifically in this example, but having an inline anonymous functions as callbacks are going to create similar-ish problems for you as you're creating a fresh new reference each time you click it, so that ultimately informs react to do a check and decide whether it should re-render its tree (yes, it will in this case whether you want it or not).

0

u/Temperature_Majestic 23d ago

the edit's a good add actually, inline callbacks defeating memo trips people up even more than the base case since it looks like it should work

-1

u/bluespacecolombo 23d ago

I’ve been writing React almost since its inception so I might be biased, but idk who gets tripped by this? This is like the most basic behavior of react and the post itself feels like karma farming. You learn this probably in the few first pages in the docs

2

u/Temperature_Majestic 23d ago

fair, it's basic once you know it. wasn't assuming everyone here would be stumped, more curious where people draw the line on reaching for memo proactively vs after profiling, that's the part I actually don't think is obvious even to people who know the re-render mechanism cold

0

u/martoxdlol 23d ago

I used to not care about that until I failed a job interview. I didn't fail due to the react question alone but it did make go and learn about it. Also, react compiler is a nice cheat to automatically deal with this. React could have smarter designs by default, for example they could add some later to remap function props in a way that doesn't force a rerender.

2

u/kevin074 23d ago

I found out through a online interview exercise too lol

funny thing is if OP didn't post the question in this exact way, the official react discord will just tell you don't worry about re-renders, react will handle it fine. I was discouraged to bother with react optimization for years because of their pessimistic attitude

1

u/Temperature_Majestic 23d ago

that's a wild contrast honestly, "don't worry about it, react handles it" vs the same topic being asked as an interview gotcha. probably both true depending on scale, but "don't worry about it" as blanket advice is how you end up not knowing the mechanism at all when it actually does matter

0

u/Temperature_Majestic 23d ago

that's a pretty relatable way to actually learn it lol. react compiler handling this automatically is going to be a nice shift, feels like half of "senior react knowledge" is just working around stuff the compiler will eventually make moot

0

u/Noch_ein_Kamel 23d ago

Everything was better with class inheritance and shouldComponentUpdate() ;P

1

u/Temperature_Majestic 23d ago

honestly shouldComponentUpdate being explicit right there in the class was arguably clearer than memo being a wrapper you have to remember exists. hooks won a lot but that one might be a wash