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