Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Parent rerenders unnecessarily on Child-to-Parent setter call, while child rerenders only when changes occur. #28287

Closed
taku-271 opened this issue Feb 9, 2024 · 6 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@taku-271
Copy link

taku-271 commented Feb 9, 2024

React version: 18

Steps To Reproduce

  1. Define state in the parent component.
const [state, setState] = useState();
  1. Render the child component and pass the function to modify the parent component's state to the child component.
<ChildComponent setState={setState} />
  1. Calls the setter but does not change the value of state.
setState((state) => state + 0);

Link to code example: https://codesandbox.io/p/sandbox/rendering-test-vts4v2?file=%2Fsrc%2FApp.js%3A3%2C20

The current behavior

The parent component rerenders even if the state remains unchanged.
When the parent component rerenders only due to unchanged state, the child component does not rerender.
image

If the value of state has been changed, the child components are rerendered.
image

The expected behavior

The parent component should not rerender if the state remains unchanged.
If the parent component rerenders, the child component should also rerender.

@taku-271 taku-271 added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Feb 9, 2024
@markerikson
Copy link
Contributor

This is expected behavior.

React queues state updates, and applies them in the render phase. If the new state value returned is identical to the previous value, React will bail out of the render for that component. If nothing else forced the component to render (its parent, other state updates, etc), then React will bail out of the render completely under the assumption that the output will be identical and there won't be any updates to apply.

See my post A (Mostly) Complete Guide to React Rendering Behavior for details.

@dai-shi
Copy link
Contributor

dai-shi commented Feb 9, 2024

The parent component rerenders even if the state remains unchanged.

But, it happens only when it rendered previously. This is a bit interesting. There might be some cases, useState's early bail-out doesn't work. (or, it can be said, it works, but some others trigger rendering?)

FWIW, if you used useReducer the behavior would be more consistent. (because there's no early bail-out.) 😄
(I've got quite a few questions about this kind of behavior in Jotai.)

@markerikson
Copy link
Contributor

@dai-shi yeah, I'm distinguishing between "bail out while rendering", vs the "early bailout while queuing the state update" behavior. (I know the early bailout exists, but I've tried to trace the code path a couple times and it's pretty confusing.)

@abdelhakimrafik
Copy link

When the parent component's state changes, it causes a re-render of the entire tree. If this change is followed by another unchanged state, the parent re-renders. However, subsequent unchanged state triggers do not cause a re-render.
image

@markerikson
Copy link
Contributor

@abdelhakimrafik yep, that's not documented, but that matches the behavior I've seen in the past.

@rickhanlonii
Copy link
Member

This is actually documented in the useState docs under Caveats.

If the new value you provide is identical to the current state, as determined by an Object.is comparison, React will skip re-rendering the component and its children. This is an optimization. Although in some cases React may still need to call your component before skipping the children, it shouldn’t affect your code. [emphasis mine]

The reason this is happening in this cases is because, in order to bail out, we need to eagerly compute the last value the reducer returned with the new value the reducer returns, because reducers can close over props. In this case, when you first click "unchanged state" the last reducer rendered was setState(state => state +1), so when we compare it to the result of setState(state => state + 0), they're different . When you click again, the last reducer was setState(state => state + 0), so they match and we can bail out. Note that setState callbacks essentially create an in-line reducer, and running the last reducer is necessary because it may have closed over props.

This behavior might seem weird, but it was necessary to fix a bug where we would eagerly bail out in cases that would shouldn't, actually breaking the app.

useReducer is consistent because it never bails out. We tried to bail out for useReducer, but there were many bugs with it, so we removed the optimization.

There may be ways we could improve this in the future, which is why it's important to not depend on the behavior and ensure rendering a component is Pure. React needs to be able to control when, and how many times, a component is rendered for performance and UX improvements. For the latest information on which cases are supported and how React currently works, tests in the React repo are the best source.

Closing as this behavior is expected and documented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants