This post is a translation from React author Dan Abramov’s Hot Issue, with a link at the end. This article is easy to read and easy to read, so please share it with us. If you are interested in React18, please feel free to discuss it.
An overview of the
React 18 adds out-of-the-box performance improvements by performing more batch processing by default, eliminating the need for manual batch updates in application or library code. This article will explain what batch processing is, how it used to work, and what has changed.
Note: This is an in-depth feature that we don’t want most users to have to consider. But it may be closely related to evangelists and react library developers.
What is batch processing
React groups multiple status updates into a single re-render for better performance.
For example, if you have two status updates in the same click event, React will always batch them into a re-render. If you run the following code, you’ll see that React only performs one render per click, even though you set the state twice:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c= > c + 1); // Does not re-render yet
setFlag(f= >! f);// Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
</div>
);
}
Copy the code
- ✅ demo: React 17 batches in an event handler. (Note that each click in the console renders once.)
This is very useful for performance because it avoids unnecessary re-rendering. It also prevents your component from rendering a “half-finished” state that updates only one state variable, which can lead to errors. It may remind you of a restaurant waiter who doesn’t run to the kitchen while you choose your first dish, but instead waits for you to complete your order.
However, React batch update times are inconsistent. For example, if you need to fetch data and then update the state on handleClick, React does not update in batches, but instead performs two separate updates.
This is because React used to only batch update during browser events (like clicks), but here we update the state after the event has already been processed (in the fetch callback) :
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() = > {
// React 17 and earlier does NOT batch these because
// they run *after* the event in a callback, not *during* it
setCount(c= > c + 1); // Causes a re-render
setFlag(f= >! f);// Causes a re-render
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
</div>
);
}
Copy the code
Prior to React 18, we only made batch updates during React event handlers. By default, React does not batch promises, setTimeout, native Event Handlers, or other events that React does not batch by default.
What is automatic batching?
Starting with createRoot in React 18, all updates will be automatically batched, no matter where they come from.
This means that updates in Timeouts, Promises, Native Event Handlers or any other event will be batched in the same way as updates in React events. We hope this results in less rendering work and thus better performance in your application:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
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!)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black}} ">{count}</h1>
</div>
);
}
Copy the code
- ✅ demo: React 18createRoot batching outside of event handling! (Note that each click in the console renders once!)
- 🟡 demo: React 18 with LegacyRender retains the old behavior (note that there are two renders per click in the console.)
Note: As part of the React 18 adoption, you are expected to upgrade to createRoot. The render of the old behavior exists only to make it easier to run production experiments on both versions.
React automatically updates in batches no matter where updates occur, so:
function handleClick() {
setCount(c= > c + 1);
setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
}
Copy the code
Same as this :(setTimeout)
setTimeout(() = > {
setCount(c= > c + 1);
setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
}, 1000);
Copy the code
Same as this :(fetch)
fetch(/ *... * /).then(() = > {
setCount(c= > c + 1);
setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
})
Copy the code
The behavior is the same :(addEventListener)
elm.addEventListener('click'.() = > {
setCount(c= > c + 1);
setFlag(f= >! f);// React will only re-render once at the end (that's batching!)
});
Copy the code
Note: React updates in batches only in secure and stable scenarios. For example, React ensures that for each user-initiated event (such as a click or keystroke), the DOM is fully updated before the next event. For example, this ensures that a form that is disabled at submission time cannot be submitted twice.
What if you don’t want to batch?
In general, batch processing is safe, but some code may rely on reading something from the DOM immediately after a state change. For these use cases, you can opt out of batch using reactdom.flushsync () :
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() = > {
setCounter(c= > c + 1);
});
// React has updated the DOM by now
flushSync(() = > {
setFlag(f= >! f); });// React has updated the DOM by now
}
Copy the code
We don’t want this to happen too often.
Does it have any effect on Hooks?
If you use Hooks, we expect automatic batching to “work” in the vast majority of cases. (If not, let us know!)
Is there any impact on Classes?
Remember that updates during the React event handler are always batch, so no changes are made to these updates.
There are edge cases in class components, which can be a problem.
The class component has an implementation quirk that synchronously reads status updates within events. This means that you will be able to read this.state between calls to setState:
handleClick = () = > {
setTimeout(() = > {
this.setState(({ count }) = > ({ count: count + 1 }));
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) = > ({ flag: !flag }));
});
};
Copy the code
In React 18, that’s no longer the case. Because all setTimeout updates are batch, React does not render the result on the first synchronous call to setState — rendering takes place in the next browser schedule. So rendering hasn’t happened yet:
handleClick = () = > {
setTimeout(() = > {
this.setState(({ count }) = > ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) = > ({ flag: !flag }));
});
};
Copy the code
See the code.
If this is a hindrance to upgrading to React 18, you can use reactdom.flushsync to force updates, but we recommend caution:
handleClick = () = > {
setTimeout(() = > {
ReactDOM.flushSync(() = > {
this.setState(({ count }) = > ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) = > ({ flag: !flag }));
});
};
Copy the code
See the code.
This issue does not affect function components with Hooks because setting state does not update existing variables from useState:
function handleClick() {
setTimeout(() = > {
console.log(count); / / 0
setCount(c= > c + 1);
setCount(c= > c + 1);
setCount(c= > c + 1);
console.log(count); / / 0
}, 1000)
Copy the code
While this behavior may be surprising when you adopt Hooks, it paves the way for automatic batching.
unstable_batchedUpdates
How to do?
Some React libraries use this unlogged API to force batch processing of setState external event handlers:
import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() = > {
setCount(c= > c + 1);
setFlag(f= >! f); });Copy the code
This API still exists in 18, but it is no longer needed because batch processing occurs automatically. We won’t remove it in 18, although it may be removed in future major releases after popular libraries no longer depend on its existence.
Original address: github.com/reactwg/rea…