Git Product home page Git Product logo

Comments (7)

leadq avatar leadq commented on April 30, 2024 1

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]);

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.

AbdulHadi806 avatar AbdulHadi806 commented on April 30, 2024

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.

Mayvis avatar Mayvis commented on April 30, 2024

@leadq maybe you can see this thread that sophiebits answer your question.

#14810

from react.

leadq avatar leadq commented on April 30, 2024

@leadq maybe you can see this thread that sophiebits answer your question.

#14810

I've checked the thread. But, no one explained the extra rerender at the end of that thread

from react.

Mayvis avatar Mayvis commented on April 30, 2024

@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.

leadq avatar leadq commented on April 30, 2024

@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.Screenshot_20240430_091524_Chrome.jpg

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.

kei444666 avatar kei444666 commented on April 30, 2024

@leadq
I've reviewed the source code regarding this issue and discovered the following:

  • The actual implementation of useState's setState is dispatchSetState.
    const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
    ): any);
    queue.dispatch = dispatch;
    return [hook.memoizedState, dispatch];
  • 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.
    if (is(eagerState, currentState)) {
    // Fast path. We can bail out without scheduling React to re-render.
    // It's still possible that we'll need to rebase this update later,
    // if the component re-renders for a different reason and by that
    // time the reducer has changed.
    // TODO: Do we still need to entangle transitions in this case?
    enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
    return;
  • 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.
    if (
    fiber.lanes === NoLanes &&
    (alternate === null || alternate.lanes === NoLanes)
    ) {
    // The queue is currently empty, which means we can eagerly compute the
    // next state before entering the render phase. If the new state is the
    // same as the current state, we may be able to bail out entirely.
    const lastRenderedReducer = queue.lastRenderedReducer;
    if (lastRenderedReducer !== null) {
    let prevDispatcher = null;
    if (__DEV__) {
    prevDispatcher = ReactSharedInternals.H;

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,

if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
// Check if this update is part of a pending async action. If so, we'll

leading to didReceiveUpdate becoming true

export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}

When didReceiveUpdate is false, bailoutHooks()'s removeLanes() is executed

if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

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.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.