React: Batching update in react-hooks

Created on 17 Nov 2018  ·  33Comments  ·  Source: facebook/react

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:

set all counters 
set counter 
set counter 
render 

console2:

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

Most helpful comment

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.

All 33 comments

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.

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

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

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.

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

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}

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

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

@rolandjitsu : Per Dan's comment earlier:

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

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

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

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

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.

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.

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(...); }).

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..
})

?

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

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

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

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

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

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

@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

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

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

What about functional version of useState.

setState( newItem => [...items, newItem]);
setState(4);

So as per @gaearon -

Updates will be queued and later executed in the order they were called.

This was some twitter post I remember. Now, what can I assume of this mix of two calls for setState in hooks?

I'm not sure if I understand how batching actually works in react.

I understand that if some component uses useState twice and I call setState of both inside batch call, they'll be batched together and the component will be re-rendered only once.

That's cool, but my question is if there is any benefit of using batch if I know some callback will result in updating many different components.

Eg. let's say I've got some subscription channel hook. A lot of different components can use it and 'listen' to updates in this channel and assign new channel values to their inner state.

In such a case, every time channel value is changed, potentially a lot of various components will update their states.

Inner implementation of publishing changes would be like

function publishNewValue(newValue) {
  subscribtion.listeners.forEach(listener => listener.publish(newValue))
}

In such a case, does wrapping such .forEach call inside batch callback give me any benefit after all? (let's assume I've got 10 different components listening for changes, where each of them will update single inner state value after each change)

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?

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?

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.

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

@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

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

Thanks @markerikson I will surely read it.

Was this page helpful?
0 / 5 - 0 ratings