Automatic batching for fewer renders in React 18 #21
Replies: 13 comments 37 replies
-
This is a fantastically clear piece of writing, @gaearon. I love the sandbox examples. I was — admittedly — unaware of the different behaviors but feel like I could explain it after this description. This post has a secondary effect of clarifying |
Beta Was this translation helpful? Give feedback.
-
TL;DR: Is this change intended? -act(() => {
+await act(async () => {
button.click()
})
// assert on effects scheduled by click() In React 17 and below Now that passive effects from discrete events schedule a sync callback as opposed to a normal passive effect, they're no longer flushed in sync button.click();
+await null;
// assert on effects scheduled by click() I did some digging and I returned to the original behavior with diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
index 91ce4c9c5f..59c7b10a77 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
@@ -193,8 +193,8 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
(isSchedulerMocked === false || previousIsSomeRendererActing === false)
) {
// we're about to exit the act() scope,
- // now's the time to flush effects
- flushWork();
+ // now's the time to flush effects and sync callbacks
+ ReactDOM.flushSync(flushWork);
}
onDone();
} catch (err) { though it does feel heavy handed (I just want to full patch with updated testsdiff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
index d2ea73e098..13d4c49ee8 100644
--- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
@@ -150,7 +150,7 @@ function runActTests(label, render, unmount, rerender) {
expect(Scheduler).toHaveYielded([100]);
});
- it('flushes effects on every call', async () => {
+ it('flushes effects on every call', () => {
function App() {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
@@ -172,16 +172,16 @@ function runActTests(label, render, unmount, rerender) {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
- await act(async () => {
+ act(() => {
click();
click();
click();
});
// it consolidates the 3 updates, then fires the effect
expect(Scheduler).toHaveYielded([3]);
- await act(async () => click());
+ act(() => click());
expect(Scheduler).toHaveYielded([4]);
- await act(async () => click());
+ act(() => click());
expect(Scheduler).toHaveYielded([5]);
expect(button.innerHTML).toBe('5');
});
diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
index 91ce4c9c5f..59c7b10a77 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js
@@ -193,8 +193,8 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
(isSchedulerMocked === false || previousIsSomeRendererActing === false)
) {
// we're about to exit the act() scope,
- // now's the time to flush effects
- flushWork();
+ // now's the time to flush effects and sync callbacks
+ ReactDOM.flushSync(flushWork);
}
onDone();
} catch (err) { I'm dreadding the confusion and churn this would add at which point it would be more reasonable to just tell people to always use At the same time you also added a |
Beta Was this translation helpful? Give feedback.
-
Awesome post @gaearon, the explanation with examples is super helpful. This is gonna be a big performance boost 🔥 . This also means that with react 18 Some suggestions
We might want to mention here that it's only for cases where automatic batching was not happening earlier just to remind the user?
can we add a snippet here just to make it more clear? function handleClick() {
console.log("=== click ===");
fetchSomething().then(() => {
// React 18 WITHOUT createRoot does not batches these:
setCount((c) => c + 1); // Causes a re-render
// count = 0, flag false
console.log(count, flag);
setFlag((f) => !f); // Causes a re-render
// count = 0, flag false
console.log(count, flag);
});
} "they always see a "snapshot" of state from a particular render" Also to confirm though |
Beta Was this translation helpful? Give feedback.
-
Related question: will |
Beta Was this translation helpful? Give feedback.
-
This is definitely one of my favorite features for performance improvement. Currently, we are batching const [formState, setFormState] = React.useState({
isDirty: false,
isTouched: false,
touchedFields: [],
...
});
setFormState({
...formState,
isTouched: true,
}) This is kind of close to batch update, except it has a problem with const { formState: { touchedFields } } = useForm()
useEffect(() => { console.log(touchedFields) }, [touchedFields]) // not working
useEffect(() => { console.log(formState.touchedFields) }, [formState]) // working, but it fire other formState gets affected as well Just to validate my assumption, with this new release. we can do the following: any sync state update will result in a single batch, but it will break the batch and result in multiple state updates with any async update. const [isDirty, setIsDirty] = React.useState(false);
const [dirtyFields, setDirtyFields] = React.useState([]);
const Test = async () => {
setIsDirty(true);
// anything execution except async action, which will break the batch?
setDirtyFields([1,2,3,4]);
} |
Beta Was this translation helpful? Give feedback.
-
How does this affect the timing of state updates queued in the commit and passive effect phases? My understanding is that currently:
Any changes there? |
Beta Was this translation helpful? Give feedback.
-
What happens if a given logical batch takes longer than 5ms and there is other work do be done? Does it get split or paused then continued? |
Beta Was this translation helpful? Give feedback.
-
It would be helpful to explain where the state is being set in this example and how it is "outside" the event handler. It can be confusing to the reader since it is happening inside the |
Beta Was this translation helpful? Give feedback.
-
This is very well-explained, @gaearon! You are an incredible teacher. One small thing that could be a little clearer in this example: function handleClick() {
fetchSomething().then(() => {
- // React 17 and earlier does NOT batch these:
+ // React 17 and earlier does NOT batch these because they are now inside a callback:
setCount(c => c + 1); // Causes a re-render
setFlag(f => !f); // Causes a re-render
});
}
or something like that! For the people who are new to batching, it may be confusing that |
Beta Was this translation helpful? Give feedback.
-
One other question on the implementation and behavior here, because I may have had a mistaken impression: Will React batch updates across multiple event loop ticks, or only within the same event loop tick? As an example: setState(1);
setTimeout(() => {
setState(2)
}, 0) Here these two state updates should be queued within a couple milliseconds of each other, but they occur in different event loop ticks because the second is inside a timeout. I believe I saw a mention somewhere that React is now using "microtasks" to execute batched state updates at the end of one event loop tick, which suggests that this would still result in two separate renders. Can someone confirm the expected behavior? I know I also saw a mention of "5ms" timing somewhere, but I think that was in reference to how long React will work before yielding back to the browser, and I may have misinterpreted that as "React will batch updates across ticks that occur within 5ms of each other". |
Beta Was this translation helpful? Give feedback.
-
I'm trying to understand the Class Component example using Should it be logging the partial update we see in the v17 version? As far as I can tell, the demo logs the the full state change Am I missing the intent of |
Beta Was this translation helpful? Give feedback.
-
Firstly, thank you so much for this info @gaearon , I really appreciate the clarity on batching, it's absolutely crucial information! I hope it's ok if I ask one more question in this old discussion.
I want to try to get extra confirmation on what assumptions can and can't be made about when state changes will take effect. This quote is really helpful but I still feel that I'm in the dark about state changes that don't happen from user initiated events e.g. setTimeout callbacks, promise callbacks etc. Basically, I just want to know when it is and isn't safe to read state and assume it's up to date. Clarity on any of the following would be amazing:
I saw your twitter posts from 2019 about mousemove being different, and you linked to a list of 'interactive' events which are those that we can assume will be flushed before the next event, but unfortunately that link is broken now so I can't see that list. Even if the link wasn't broken, I still wanted to ask, is any of this considered an implementation detail or can it be relied upon? Apologies for the long question, but I really feel this is crucial information right? Without knowing when it is and isn't safe to read state, and assume it's up-to-date, I'm struggling to use react with confidence (although your confirmation on user initiated events really helped!) |
Beta Was this translation helpful? Give feedback.
-
Can somebody give me an explanation how can React batch update states from asynchronous code like this? I mean how is it possible to know when the batch should be flushed already? 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!)
});
} Does React uses a timeout internally or something? |
Beta Was this translation helpful? Give feedback.
-
Overview
React 18 adds out-of-the-box performance improvements by doing more batching by default, removing the need to manually batch updates in application or library code. This post will explain what batching is, how it previously worked, and what has changed.
What is batching?
Batching is when React groups multiple state updates into a single re-render for better performance.
For example, if you have two state updates inside of the same click event, React has always batched these into one re-render. If you run the following code, you’ll see that every time you click, React only performs a single render although you set the state twice:
This is great for performance because it avoids unnecessary re-renders. It also prevents your component from rendering “half-finished” states where only one state variable was updated, which may cause bugs. This might remind you of how a restaurant waiter doesn’t run to the kitchen when you choose the first dish, but waits for you to finish your order.
However, React wasn’t consistent about when it batches updates. For example, if you need to fetch data, and then update the state in the
handleClick
above, then React would not batch the updates, and perform two independent updates.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 fetch callback):
Until React 18, we only batched updates during the React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.
What is automatic batching?
Starting in React 18 with
createRoot
, all updates will be automatically batched, no matter where they originate from.This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events. We expect this to result in less work rendering, and therefore better performance in your applications:
createRoot
batches even outside event handlers! (Notice one render per click in the console!)render
keeps the old behavior (Notice two renders per click in the console.)React will batch updates automatically, no matter where the updates happen, so this:
behaves the same as this:
behaves the same as this:
behaves the same as this:
What if I don’t want to batch?
Usually, batching is safe, but some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use
ReactDOM.flushSync()
to opt out of batching:We don't expect this to be common.
Does this break anything for Hooks?
If you’re using Hooks, we expect automatic batching to "just work" in the vast majority of cases. (Tell us if it doesn't!)
Does this break anything for Classes?
Keep in mind that updates during React event handlers have always been batched, so for those updates there are no changes.
There is an edge cases in class components where this can be an issue.
Class components had an implementation quirk where it was possible to synchronously read state updates inside of events. This means you would be able to read
this.state
between the calls tosetState
:In React 18, this is no longer the case. Since all of the updates even in
setTimeout
are batched, React doesn’t render the result of the firstsetState
synchronously—the render occurs during the next browser tick. So the render hasn’t happened yet:See sandbox.
If this is a blocker to upgrading to React 18, you can use
ReactDOM.flushSync
to force an update, but we recommend using this sparingly:See sandbox.
This issue doesn't affect function components with Hooks because setting state doesn't update the existing variable from
useState
:While this behavior may have been surprising when you adopted Hooks, it paved the way for automated batching.
What about
unstable_batchedUpdates
?Some React libraries use this undocumented API to force
setState
outside of event handlers to be batched:This API still exists in 18, but it isn't necessary anymore because batching happens automatically. We are not removing it in 18, although it might get removed in a future major version after popular libraries no longer depend on its existence.
Beta Was this translation helpful? Give feedback.
All reactions