Batching in React
Shivam Jha / April 22, 2022
5 min read
One might think that React's useState
hook is the simplest hook. Yet, there are some complexities.
What is batching ?
Batching is when multiple calls to setState
are grouped into only one state update
function App() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
useEffect(() => {
// only output once per click
console.log({ count, flag })
}, [count, flag])
const handleClick = () => {
// Here, react will re-render only once
// Hence, the state updates are `batched`
setCount(c => c + 1)
setFlag(f => !f)
}
return (
<div className='App'>
<button onClick={handleClick}>Click Me!</button>
<h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
</div>
)
}
- ✅ See demo (batching inside event handlers) (Note on click of button, both count and flag changes, but only one console output)
Why Batching ?
- Great for performance, since avoids un-necessary re-renders.
- Prevents any component from rendering "half-applied" state updates, which may lead to bugs.
Inconsistent Batching Behavior
However, React was (more about that later) not consistent about batching. For example, in an async function / promise based API,
React would not batch the updates & independent updates would happen (performing two setState
calls).
// little async function
const sleep = () => new Promise(resolve => setTimeout(resolve, 200))
export default function App() {
const [flag, setFlag] = useState(true)
const [count, setCount] = useState(0)
const handleClick = async () => {
// mimicing some async call
// (ex, fecthing data from server, etc.)
await sleep()
setFlag(f => !f)
setCount(c => c + 1)
}
useEffect(() => {
// in this case, two console logs can be seen
// since `setState` is called inside an asynchronous function
// So, React would not batch the updates, and perform two independent updates.
console.log({ count, flag })
// whenever `flag` or `count` changes, do somethig!
}, [count, flag])
return (
<>
<h2>React's Batching Behavior while inside async callbacks</h2>;
<p>Count: {count}</p>
<button
onClick={handleClick}
style={{ backgroundColor: flag ? 'orange' : 'blue', color: '#fff' }}
>
Click me!
</button>
</>
)
}
- ⚠️ See demo (not batching updates inside async function) (Note on click of button, two lines get printed on console)
Forced batching in async functions
To force setState
to batch updates out of event handlers, unstable_batchedUpdates
(an undocumented API) can be used:
import { unstable_batchedUpdates } from 'react-dom'
unstable_batchedUpdates(() => {
setCount(c => c + 1)
setFlag(f => !f)
})
This is because React used to only batch updates during a browser event (like click), but here we're updating the state after the event has already been handled (in aync function):
For demo, see React 17: forced batching outside of event handlers
Opt out of automatic batching
Some code may depend on reading something from the DOM immediately after a state change. For those use cases, ReactDOM.flushSync can be used to opt out of batching
Continuing with our previous example,
function App() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
useEffect(() => {
console.log({ count, flag })
}, [count, flag])
const handleClick = () => {
// setCount((c) => c + 1);
// Force this state update to be synchronous.
ReactDOM.flushSync(() => setCount(c => c + 1))
// By this point, DOM is updated.
setFlag(f => !f)
}
return (
<div className='App'>
<button onClick={handleClick}>Click Me!</button>
<h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
</div>
)
}
See ⚠️ ReactDOM.flushSync: Opt out of automatic batching in event handlers
- However,
ReactDOM.flushSync
is not common & should be sparingly used.
flushSync flushes the entire tree and actually forces complete re-rendering for updates that happen inside of a call, so you should use it very sparingly. This way it doesn’t break the guarantee of internal consistency between props, state, and refs.
To read more about async behavior of this API & why setState
is asynchronous, check out this awesome discussion RFClarification: why is setState asynchronous? #11527
Automatic Batching in React 18
React 18 includes some out-of-the-box improvements with ReactDOMClient.createRoot
,
which includes support for automatic batching
Starting in React 18, all updates will be automatically batched, no matter where they originate from.
So, call to setState
inside of event handlers, async functions, timeouts or any function will batch automatically (same as inside react events)
This will result in less rendering, and therefore better performance in react applications
function handleClick() {
fetchSomething().then(() => {
// React 18 and later DOES batch these:
setCount(c => c + 1)
setFlag(f => !f)
// React will only re-render once at the end (that's batching!)
})
}
- Note that this automatic batching behavior will only work in React 18 with
ReactDOM.createRoot
- React 18 with legacy
ReactDOM.render
keeps the old behavior - To read more about Automatic batching in React 18, see Automatic batching for fewer renders in React 18 #21