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

Batching update in react-hooks #14259

Closed
smmoosavi opened this issue Nov 17, 2018 · 41 comments
Closed

Batching update in react-hooks #14259

smmoosavi opened this issue Nov 17, 2018 · 41 comments

Comments

@smmoosavi
Copy link

smmoosavi commented Nov 17, 2018

Do you want to request a feature or report a bug?

bug + improvement

What is the current behavior?

Sometimes state updates caused by multiple calls to useState setter, are not batched.

sandbox1: https://codesandbox.io/s/8yy0nw2m28
sandbox2: https://codesandbox.io/s/1498n44yr3

Step to reproduce: click on increment all text and check the console

diff:

// sandbox1
  const incAll = () => {
    console.log("set all counters");
    incA();
    incB();
  };

// sandbox2
  const incAll = () => {
    setTimeout(() => {
      console.log("set all counters");
      incA();
      incB();
    }, 100);
  };

console1:

render 
set all counters 
set counter 
set counter 
render 

console2:

render 
set all counters 
set counter 
render 
set counter 
render 

What is the expected behavior?

Render function should be called once after multiple calls of setState

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

react: 16.7.0-alpha.2

@markerikson
Copy link
Contributor

This appears to be normal React behavior. It works the exact same way if you were to call setState() in a class component multiple times.

React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().

I think there's plans long-term to always batch events, but not sure on the details.

@gaearon
Copy link
Collaborator

gaearon commented Nov 18, 2018

Yeah, this isn’t different from behavior in classes.

You can opt into batching with ReactDOM.unstable_batchedUpdates(() => { ... }) in the meantime.

@smmoosavi
Copy link
Author

Yeah, this isn’t different from behavior in classes.

when we are using setState in class, all changed states are batched in one setState call.

this.setState({a: 4, b: 5});

but when we are using useState hooks, set state functions called seperately:

setA(4);
setB(5);

I think it is necessary to have automatic batching for useState hooks.

@markerikson
Copy link
Contributor

@smmoosavi : no, React handles both setState and useState the same way. If you make multiple calls to either within a React event handler, they will be batched up into a single update re-render pass.

@smmoosavi
Copy link
Author

smmoosavi commented Dec 27, 2018

If you make multiple calls to either within a React event handler, they will be batched up into a single update re-render pass.

if the event handler is an async function, they will not be batched up.

more real-world example:

https://codesandbox.io/s/8lp7y5wj09

  const onClick = async () => {
    setLoading(true);
    setData(null);
    // batched render happen
    const res = await getData();
    setLoading(false);
    // render with bad state
    setData(res);
  };
console.log("render", loading, data);

output:

render false null
// click
render true null
render false null   // <-- bad render call
render false {time: 1545903880314}
// click
render true null
render false null   // <-- bad render call
render false {time: 1545904102818}

@markerikson
Copy link
Contributor

@smmoosavi : yes, that matches everything I've said already.

React wraps your event handlers in a call to unstable_batchedUpdates(), so that your handler runs inside a callback. Any state updates triggered inside that callback will be batched. Any state updates triggered outside that callback will not be batched. Timeouts, promises, and async functions will end up executing outside that callback, and therefore not be batched.

@rolandjitsu
Copy link

Is there a way though to batch state updates in async calls?

@markerikson
Copy link
Contributor

@rolandjitsu : Per Dan's comment earlier:

You can opt into batching with ReactDOM.unstable_batchedUpdates(() => { ... })

@rolandjitsu
Copy link

@markerikson sorry, I did not pay attention to the comments and I ignored them once I saw unstable_ 🤦‍♂️

@devthejo
Copy link

devthejo commented Mar 2, 2019

To solve this problem I use useReducer instead of useState and my I do my "batching" by grouping my data updates in the reducer ;)

@gaearon
Copy link
Collaborator

gaearon commented Mar 2, 2019

Yep, using the reducer is recommended for anything other than a trivial update.

@smmoosavi
Copy link
Author

You can replace useState with useReducer when you are writing hooks from scratch. but when you are using third-party hooks (other libs) which return setState you can't use useReduceer and you have to use unstable_batchedUpdates.

@tqwewe
Copy link

tqwewe commented Mar 19, 2019

Spent some time at work today trying to find a good solution to this. Ended up merging my two states (isAuthenticating and user) into an object to solve this but I would certainly love to see some kind of function such as useBatch(() => {}) become available to use.

@nmain
Copy link

nmain commented Apr 8, 2019

I find myself running into a lot of use cases where two state values are usually completely separate, but occasionally might be updated at the same time. useReducer is way too heavy for these and I'd much rather have a stable batch(() => { changeFoo(...); changeBar(...); }).

@andrevinsky
Copy link

Hey, what about doing it with a batch wrapper call, like Redux does it now:

const [ state, dispatch, batch ] = useReducer(reducer, initialState);

batch(() => {
  dispatch(..);
  dispatch(..);
  // etc..
})

?

@smmoosavi
Copy link
Author

smmoosavi commented Apr 17, 2019

@nmain
I was writing batched-hook lib. I think It can help you.

import React from 'react';
import ReactDOM from 'react-dom';
import { install } from 'batched-hook';
 
// `uninstall` function will restore default React behavior.
const uninstall = install(React, ReactDOM);

@nmain
Copy link

nmain commented Apr 17, 2019

Thanks, I'll take a look into that. 👍

@smmoosavi
Copy link
Author

@gaearon
how we can detect if already we are inside unstable_batchedUpdates?

@W3stside
Copy link

@smmoosavi this was explained already, inside event handlers it's always wrapped in unsafe batch, else it's not.

@smmoosavi
Copy link
Author

@smmoosavi this was explained already, inside event handlers it's always wrapped in unsafe batch, else it's not.

I mean programmatically. something like this:

function inBatchedMode() {
  return workPhase !== NotWorking
}

@rzec-r7
Copy link

rzec-r7 commented Jun 26, 2019

@gaearon So you make mention that the only way to make sure that multiple calls to useState update handlers will not cause multiple re-renders is by using ReactDOM.unstable_batchedUpdates() (which does seem to work), is it possible that this might change / be removed in a patch or minor release?

I assume yes since is it pre-fixed with unstable_ which make me nervous in using it however it is pretty much the only way I can see being able to use my debounce version of useState without causing multiple re-renders (which in the case causes multiple APIs requests which I can't have).

@markerikson
Copy link
Contributor

@rzec-r7 : we actually started using this in React-Redux v7, because Dan explicitly encouraged us to. I believe one of his comments was "Half of Facebook depends on this, and it's the most stable of the 'unstable' APIs".

See https://react-redux.js.org/api/batch

@igor10k
Copy link

igor10k commented Aug 5, 2019

@gaearon any insight on why batching is still considered unstable? Are there any use-cases where it's known to cause problems?

@hg-pyun
Copy link

hg-pyun commented Aug 16, 2019

This is also likely to happen when the state is changed within a window event.

@kalbert312
Copy link

How do I batch updates with a custom reconciler? Using the lib: https://github.com/inlet/react-pixi and ReactDOM.unstable_batchedUpdates doesn't work here. I'm guessing just stick with useReducer?

@markerikson
Copy link
Contributor

Batching is reconciler-dependent. That's why React-Redux has to semi-depend on importing unstable_batchedUpdates from either ReactDOM or React Native, depending on what platform we're running on. So, if the reconciler you're using doesn't export a form of unstable_batchedUpdates, then yeah, you're pretty much out of luck.

@ramHruday
Copy link

I'm having the same question @pie6k has - what if I have a global data stream that many components subscribe to and receive data from? Is there any way to batch those component updates, or would I have to use sth like rAF or setTimeout instead?

In my case I had the same global store to update and delete from various setTimeouts, in that case, setSomevariable wasn't batching..

the fix which worked for me was using the prev state of
seSomeVariable((prev) => {prev.add(something); return prev});

Actual working is confusing me..but its working ;)

@bhaidar
Copy link

bhaidar commented Nov 14, 2020

@markerikson What a useful post! Thank you.

I wonder what happens when a setState() is called inside the setTimeout() handler. The setTimeout is placed inside a new Promise() and the last statement of the handler is a call to resolve() the promise. There is also a then() to run after the promise is fulfilled.

At what moment of time, will React re-render the component?

Thanks

@markerikson
Copy link
Contributor

@bhaidar : earlier this year I wrote an extensive post on A (Mostly) Complete Guide to React Rendering Behavior, specifically to answer questions like yours :) I'd encourage you to read through it in detail.

@bhaidar
Copy link

bhaidar commented Nov 14, 2020

Thanks @markerikson I will surely read it.

@dcheung2
Copy link

Am I the only one expect React.useCallback should do state changes in a single batch/transaction for me ?

@tqwewe
Copy link

tqwewe commented Jan 26, 2021

@dcheung2 100% agree. There may be some down sides, but coding in React I feel like it's more intuative if useCallback and useEffect did everything in a batch.

@dcheung2
Copy link

dcheung2 commented Jan 26, 2021

@acidic9
To be fair, it won't fix all the problem.
e.g.

React.useCallback(async ()=>{
    setState1(1);
    setState2(1); // likely batched, if 
    await Promise.resolve(2);
    setState1(3); // won't batched
    setState2(3); // won't batched
    await Promise.resolve(4);
    setState1(5);           //non batched
    await Promise.resolve(6);
    setState2(7);          // non batched
 }, []);

but at least React.useCallback could mitigate half of it, if they are going to it in the legacy mode without using anything unstable API.

Or it may helps some eslint rule to detect the problem.

I believe they want the blocking/concurrent mode in new React will eventually fix the cause.

@dani-mp
Copy link

dani-mp commented Jul 30, 2021

Does anyone know if the new automatic batching feature in React 18 works also with several useReducer dispatches?

Usually, I only dispatch once when something happens in my component, but some people also do something like:

dispatch({ type: "ACTION_1" });
dispatch({ type: "ACTION_2" });

Would these be batched automatically, resulting in a single render?

@markerikson
Copy link
Contributor

@dani-mp : useReducer is just another form of React's one-and-only state update implementation, so yes.

@panta82
Copy link

panta82 commented Feb 20, 2022

I just got hit by this footgun.

A codependent state update was working fine while being called from events. But the first time I called it from a resolved promise, it started crashing.

IMO this is a surprising behavior. It should work the same in both situations.

@markerikson
Copy link
Contributor

@panta82 the existing behavior is well documented. But, per reactwg/react-18#21 , multiple updates in a single event loop tick outside of React's original event handling (ie, in a .then() or after an await) will now be batched as well.

@panta82
Copy link

panta82 commented Feb 20, 2022

@markerikson This is a very positive change! Exactly what's needed.

As for the documentation, I browsed through the useState manual at https://reactjs.org/docs/hooks-state.html , and did a bunch of google searches in the vein of "useState async callback", and found nothing. I am sure there's plenty of documentation about this in GH issues and blog posts, but for me, it got lost in the noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests