Comments (7)
@leadq
I've reviewed the source code regarding this issue and discovered the following:
- The actual implementation of useState's setState is dispatchSetState.
react/packages/react-reconciler/src/ReactFiberHooks.js
Lines 1840 to 1846 in d779eba
- It's found that if the lanes bound to the fiber in dispatchSetState are not NoLanes (0), re-rendering occurs even if the eagerState remains the same.
- EagerState is checked, and if it's identical to the current value, it returns without re-rendering.
react/packages/react-reconciler/src/ReactFiberHooks.js
Lines 3341 to 3348 in d779eba
- If the lanes of the fiber bound to dispatchSetState are not NoLane, eagerState is not checked, leading to re-rendering even when the same value is set.
react/packages/react-reconciler/src/ReactFiberHooks.js
Lines 3318 to 3329 in d779eba
Why is it re-rendering even when the same value is set?
Regarding this issue, I've observed two crucial behaviors in React:
- When setState (dispatchSetState) is called, the bound fiber might refer to the current one (yet to be reflected on the screen) or the previous fiber (currently reflected on the screen). They alternate with each rendering.
- During re-rendering, the current fiber may have lanes at 0, but the previous fiber (contained in the current fiber's alternate) may have lanes at 2.
Reasons for these behaviors:
The fiber referred to by dispatchSetState at the time of the call might be the current fiber or the previous one
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.)
During re-rendering, the current fiber may have lanes at 0, but the previous fiber may have lanes at 2
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,
react/packages/react-reconciler/src/ReactFiberHooks.js
Lines 1459 to 1462 in d779eba
leading to didReceiveUpdate becoming true
react/packages/react-reconciler/src/ReactFiberBeginWork.js
Lines 3451 to 3453 in d779eba
When didReceiveUpdate is false, bailoutHooks()'s removeLanes() is executed
react/packages/react-reconciler/src/ReactFiberBeginWork.js
Lines 1180 to 1183 in d779eba
Conclusion
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.
from react.
@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.
from react.
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.
from react.
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]);
from react.
@leadq maybe you can see this thread that sophiebits answer your question.
from react.
@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
from react.
@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 react.
Related Issues (20)
- Unexpected Initial State Jump in 'useEffect" with 'setTimeout' and State Dependencies HOT 3
- React[19] Module '"react"' has no exported member 'useActionState'. HOT 2
- Bug: effect runs with stale state values outside of Concurrent React HOT 1
- Feature Request: ESLint hooks rule for accessing previous state when deriving new state
- Bug: Weird Behavior of useCallback() hook When Variables or States Are defined before and after the Callback (ES5) HOT 5
- Bug: div: `ref` is not a prop HOT 3
- Bug: useFormStatus pending state is reset when component state is updated HOT 5
- [React 19] TEST HOT 1
- `FALLBACK_THROTTLE_MS` slows tests down significantly - could it be configurable? HOT 4
- Unexpected state behavior when updating nested state objects HOT 5
- Make pre-release version naming align with semver 2 HOT 5
- [React 19] Omit the false value for custom element is an incorrect behaviour HOT 7
- Remove references to legacy class-based component architecture in error messages HOT 3
- Bug: deleting structure via DOM API breaks state updates HOT 9
- Module '"react"' has no exported member 'use' HOT 3
- Bug: Unexpected characters appear in the Chinese encoding during streaming HOT 4
- Bug: useState, state does not update inside websocket.onmessage rather the state value is preserved untill the next render HOT 2
- Bug: When input type=number, input 1-1 cannot trigger an onchange and cannot be set to an empty string HOT 2
- TestUtils.Simulate alternatives
- use hook issue HOT 8
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. ๐๐๐
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from react.