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: Nested setState and unstable_batchedUpdates (are they ignored?) #17970

Closed
salvoravida opened this issue Feb 4, 2020 · 11 comments
Closed

Comments

@salvoravida
Copy link

Nested setState and unstable_batchedUpdates (are them ignored?)

React version: 16.12

Steps To Reproduce

https://codesandbox.io/s/batchedupdates-uselayouteffect-evj8s

open profile after click, you will see 3 commit.

it seems that even if we use unstable_batchedUpdates, nested setStates called on
didUpdate/layouteffect do not get batched.

@salvoravida salvoravida added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Feb 4, 2020
@salvoravida salvoravida changed the title Bug: Nested setState and unstable_batchedUpdates (are them ignored?) Bug: Nested setState and unstable_batchedUpdates (are they ignored?) Feb 4, 2020
@markerikson
Copy link
Contributor

The sandbox isn't actually testing the behavior you think it is.

First, React already wraps all event handlers in unstable_batchedUpdates(). That's why multiple queued state updates in a single handler result in one re-render. So, the manual call to unstable_batchedUpdates() does nothing there.

Second, either way, that button click event handler is only queueing one state update, not multiple, so there's no change in behavior.

Third, each of those child components is adding an additional state update in the commit phase, but that's after the batchedUpdates() wrapper has already completed, so it's not like there's any way that could get batched.

On the other hand, what React-Redux does is call unstable_batchedUpdates(), in the commit phase, as multiple nested child components are notified and queue state updates. Very different logic.

@salvoravida
Copy link
Author

Sorry I do not think so. React redux call notifynestedsub under uselayouteffecct that is exact the problem of this issue. Open profile of this original nested redux update with connect.

Anyway it would be useful to know from react core team if this is an expected behavior, setStates sync in uselayouteffecct does not get batched, and there are many commit even if user will not see them due to sync.

@bvaughn
Copy link
Contributor

bvaughn commented Feb 4, 2020

Mark is right that unstable_batchedUpdates only groups state updates that are scheduled within the callback you pass it. (Once the updates are processed, you're no longer in the scope of unstable_batchedUpdates and normal batching behavior is applied.)

@markerikson
Copy link
Contributor

@bvaughn: just to confirm something:

If a component tree has something like this:

function Parent() {
    useLayoutEffect(() => {
        ReactDOM.unstable_batchedUpdates( () => {
            eventEmitter.trigger("update");
        });
        
    });

    // return some nested child components
}

function Child() {
    const [, forceUpdate] = useReducer(s => s + 1, 0);

    useLayoutEffect( () => {
        eventEmitter.on("update", forceUpdate);
        return () => eventEmitter.off("update", forceUpdate);
    }, []);
}

I would expect that when the parent component re-renders, all nested children would queue updates due to the event emitter triggering. But, because the calls to forceUpdate() are all within the parent's batchedUpdates(), all of those will get batched into a single re-render, which then happens synchronously as part of Parent's commit phase.

Is that a correct summary?

@salvoravida
Copy link
Author

Mark is right that unstable_batchedUpdates only groups state updates that are scheduled within the callback you pass it. (Once the updates are processed, you're no longer in the scope of unstable_batchedUpdates and normal batching behavior is applied.)

just to confirm the call stack of this issue:

ReactDOM.unstable_batchedUpdates(() => {
      setCount1(c => c + 1);
      // ....
      useLayoutEffect1(()=>{
              setCount2(c => c + 1);
               // ....
              useLayoutEffect2(()=>{
                     setCount3(c => c + 1);
                       // ....
              }) 
      })     
});

so the batches are interrupted @ first useLayoutEffect? (even if next calls are sync) ?

@bvaughn
Copy link
Contributor

bvaughn commented Feb 5, 2020

@markerikson Yes.

@salvoravida That is not the call stack. The function you pass to useLayoutEffect won't be called under the render has finished, and the render won't even start until the function you pass to unstable_batchedUpdates has finished running. (That's how updates are batched. React waits until that callback is done so that it knows no more updates are pending.)

@salvoravida
Copy link
Author

salvoravida commented Feb 5, 2020

@bvaughn so there is no way to batch all updates from an external source emitted in top down in a single commit.

useMutableSource will handle this case?

@markerikson
Copy link
Contributor

@salvoravida : The issue for React-Redux specifically is that we can't run the mapState functions for nested connected components until their parents have completed rendering, because we need the latest props to pass to mapState, and parent components might stop rendering a connected child (aka, the "zombie child" and "stale props" issues).

If those weren't a point of concern, we could just go ahead and let all components subscribe to the root Subscription, and batch all of them into a single render pass.

So, for our current implementation, we must have multiple commits to handle things correctly working down the tree.

@salvoravida
Copy link
Author

salvoravida commented Feb 5, 2020

@markerikson yes i know the issues, and i agree with you.

What i exspected is that using LayoutEffects handlers that are sync with renders, and batched updates, multiple sync commit where merged to one.

Maybe we see 4 commit into Profiler, but as they are sync, they are de-facto merged as browser will (not paint/user will not see) them until render-sync finished

@bvaughn is correct?

@markerikson
Copy link
Contributor

Yeah. In our case, they're separate render passes, all run synchronously during the commit phase. Because they're run sync, the browser won't repaint until after the commit phase is complete.

@bvaughn
Copy link
Contributor

bvaughn commented Feb 5, 2020

Maybe we see 4 commit into Profiler, but as they are sync, they are de-facto merged as browser will (not paint/user will not see) them until render-sync finished

This sounds misleading 😄 If the Profiler reports 4 commits, there were 4 commits- (which means e.g. 4 DOM mutations)- even if there was no painting between them.

I'm going to go ahead and close this issue since I think the original question has been answered!

@bvaughn bvaughn closed this as completed Feb 5, 2020
@bvaughn bvaughn added Type: Question and removed Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug labels Feb 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants