Open leadq opened 6 months ago
In React, when you update the state using useState
, React doesn't immediately update the state and re-render the component. Instead, it schedules the state update and re-rendering to occur asynchronously. This means that when you call setCount(1)
in your first scenario or setCount("2")
in your second scenario, React doesn't update the state and re-render the component immediately.
React batches state updates for performance reasons. When multiple setState
calls are made within the same synchronous event, React will batch them together and perform a single re-render at the end of the event. This is why you're seeing unexpected behavior in your console logs.
In your first scenario, when you click the button, React schedules the state update to 1, but before it re-renders the component, it logs the current count value, which is still 0. Then it re-renders the component with the updated count value of 1.
In your second scenario, similarly, React schedules the state update to "2" and logs the current count value, which is still "1" before re-rendering the component. Then it re-renders the component with the updated count value of "2".
This behavior is expected in React due to its asynchronous nature of state updates and re-renders. If you want to perform any action after the state has been updated, you should use useEffect
hook with appropriate dependencies.
But you can still try memoizing the state. However as far as I know this is expected behavior :)
const memoizedCount = useMemo(() => count, [count]);
In React, when you update the state using
useState
, React doesn't immediately update the state and re-render the component. Instead, it schedules the state update and re-rendering to occur asynchronously. This means that when you callsetCount(1)
in your first scenario orsetCount("2")
in your second scenario, React doesn't update the state and re-render the component immediately.React batches state updates for performance reasons. When multiple
setState
calls are made within the same synchronous event, React will batch them together and perform a single re-render at the end of the event. This is why you're seeing unexpected behavior in your console logs.In your first scenario, when you click the button, React schedules the state update to 1, but before it re-renders the component, it logs the current count value, which is still 0. Then it re-renders the component with the updated count value of 1.
In your second scenario, similarly, React schedules the state update to "2" and logs the current count value, which is still "1" before re-rendering the component. Then it re-renders the component with the updated count value of "2".
This behavior is expected in React due to its asynchronous nature of state updates and re-renders. If you want to perform any action after the state has been updated, you should use
useEffect
hook with appropriate dependencies.But you can still try memoizing the state. However as far as I know this is expected behavior :)
const memoizedCount = useMemo(() => count, [count]);
But I dont understand the async behaviour you mentioned. I know applying next rerender with new changes somehow asynchronus because of optimization. But I think it is not related with this. Because if you click once then wait for a minute, rerender will already be done and whereever the state value inside react closure should already be updated with next value until next click. Lets put some time between two clicks. React still rerender with same immutable value. Are you sure about your explanation? Other hand, I couldnt find any deep dive explanation about state closure implementation inside react. If you know implementation detail, please let me know.
@leadq maybe you can see this thread that sophiebits answer your question.
@leadq maybe you can see this thread that sophiebits answer your question.
I've checked the thread. But, no one explained the extra rerender at the end of that thread
@leadq As far as I know, React can’t guess the output of render() won’t change, even if you update state has the same value, it has to render() again and compare the results with the previous render(). This is the conclusion. React optimize this strategy called "eagerState" to make sure it will not re-render.
So how is the "eagerState" work?
In React, state is stored in the fiber tree, and react use double cache mechanism, there are at least two fiber trees in existence. When we mark a component A as needing an update, the "update exists" information is stored in two fiber nodes corresponding to component A in its respective fiber trees. When the first update occurs and is completed after a click, the "update exists" information is erased from one of the fibers, but it remains in the other related fiber. So, the next time component A is updated, it will still render because the "update exists" information remains in one of the fibers. However, during subsequent updates, both fibers related to component A do not have updates, allowing component A to hit eagerState and avoid rendering.
If you don't want this behavior. Just simply prevent by yourself. 😅
const handleClick = () => {
if (count === prevCount) return
setCount(1)
}
If you want more detail, you need to study the react source code by yourself.😅
Hope this can help. ;)
@leadq As far as I know, React can’t guess the output of render() won’t change, even if you update state has the same value, it has to render() again and compare the results with the previous render(). This is the conclusion. React optimize this strategy called "eagerState" to make sure it will not re-render.
So how is the "eagerState" work?
In React, state is stored in the fiber tree, and react use double cache mechanism, there are at least two fiber trees in existence. When we mark a component A as needing an update, the "update exists" information is stored in two fiber nodes corresponding to component A in its respective fiber trees. When the first update occurs and is completed after a click, the "update exists" information is erased from one of the fibers, but it remains in the other related fiber. So, the next time component A is updated, it will still render because the "update exists" information remains in one of the fibers. However, during subsequent updates, both fibers related to component A do not have updates, allowing component A to hit eagerState and avoid rendering.
If you don't want this behavior. Just simply prevent by yourself. 😅
const handleClick = () => { if (count === prevCount) return setCount(1) }
If you want more detail, you need to study the react source code by yourself.😅
Hope this can help. ;)
From my perspective, this library (which, by the way, is a great asset to have in our lives) offers some APIs for us to use. As an end user, I view this library as a black box—I expect it to function reliably and consistently as described, without needing to understand its internal workings, much like any API consumer would. According to React's documentation, setState performs certain optimizations, and if the next value remains the same after being checked by a method like Object.is, it does not re-render the component. However, the official docs also mention that there might be cases where it could still cause a re-render. This seems to be a buggy behavior, but the docs mention this only briefly, allowing us to categorize this issue as "some cases". However, there's no clarification on what these "some cases" are. If it requires looking into the source code to understand, this is indeed a significant challenge for us.
Certainly, having an in-depth knowledge of the source code and understanding how it works would be ideal. I do find myself diving into the library out of curiosity from time to time. However, my point is that using the phrase "some cases" in the official documentation feels rather precarious. It's very vague, and when I encounter a bug, I can't possibly know whether it falls into this "some cases" category. Therefore, there should be examples and clear limitations of these cases in the official documentation. I've opened this issue because maybe something is missed, and it would be beneficial if the maintainers could shed some light on this.
@leadq
I've reviewed the source code regarding this issue and discovered the following:
Regarding this issue, I've observed two crucial behaviors in React:
React places the previous fiber in the alternate of the current fiber with each rendering. Thus, the fiber bound to dispatchSetState alternates between the current and the previous one with each rendering. (This is my observation based on changes during rendering.)
The lanes of a fiber become NoLanes when bailoutHooks()'s removeLanes() is executed, which occurs when the current value matches the previous value.
Thus, when there's an update, markWorkInProgressReceivedUpdate is executed,
https://github.com/facebook/react/blob/d779eba4b375134f373b7dfb9ea98d01c84bc48e/packages/react-reconciler/src/ReactFiberHooks.js#L1459-L1462
leading to didReceiveUpdate becoming true
https://github.com/facebook/react/blob/d779eba4b375134f373b7dfb9ea98d01c84bc48e/packages/react-reconciler/src/ReactFiberBeginWork.js#L3451-L3453
When didReceiveUpdate is false, bailoutHooks()'s removeLanes() is executed
https://github.com/facebook/react/blob/d779eba4b375134f373b7dfb9ea98d01c84bc48e/packages/react-reconciler/src/ReactFiberBeginWork.js#L1180-L1183
When the fiber bound to dispatchSetState refers to the previous fiber, and the lanes of the previous fiber are at 2 (If there was an update to the value in the previous rendering), re-rendering occurs even when the same value is set for setState.
This speculation is not verified against all operations, so there might be inaccuracies.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
However, my point is that using the phrase "some cases" in the official documentation feels rather precarious. It's very vague, and when I encounter a bug, I can't possibly know whether it falls into this "some cases" category.
Firstly, huge +1 to @leadq's sentiment regarding this issue. As a React developer, such documentation feels very hand-wavy and does lend confidence to my DX.
Secondly, thank you so much @kei444666 for the deep-dive! I now at least have a very high-level understanding that this issue is related to React's fiber architecture. @sebmarkbage's https://github.com/facebook/react/issues/17474#issuecomment-560942800 from a few years ago corroborates this.
I would still love for React maintainers to chime in:
Over the years, sentiment has waxed and waned around whether unnecessary renders matter, particularly in regards to the end user experience. As someone who wants to deliver pragmatic value to users, I believe unnecessary renders are fine if it doesn't impact user experience. However, as an engineer, the extra render makes me uncomfortable. Does the React team agree it's not ideal?
Is this issue optimizable? (i.e. is it possible to completely prevent that rerender?) a. If not optimizable, why not? Is it a core limitation of the React fiber architecture? Would it require a rearchitecture?
As far as I know, react's update machanism somehow checks the new value and current value of states to optimize rerenders. And I can see this behaviour after second time.
I tried 2 scenario. I put "console.log" just above return().
The first one is, I defined a state with initial value String "1". Then I tried to set the same value on button click. It never rerendered as expected.
The second scenerio is, I defined a state with initial value String "1". Then I tried to set another value String "2" on every button click. My expectation is it will rerender to set the value "2". But after setting the value "2" once, it should never rerender for value "2" if current value still "2". However it does one more time. After then, it wont rerender. Is it expected behaviour ?
React version: 18.2.15
Steps To Reproduce
https://codesandbox.io/p/sandbox/nervous-albattani-vl3mnm?file=%2Fsrc%2FApp.js%3A12%2C31
The current behavior
it rerenders one more time with same old value. After one extra rerender, It won't rerender with same value as expected.
The expected behavior
it should never rerender with same prev value