使用挂钩时 React 批处理状态更新函数吗?

对于类组件,如果内部事件处理程序,则 this.setState调用批处理。但是,如果在事件处理程序之外更新状态并使用 useState钩子,会发生什么情况呢?

function Component() {
const [a, setA] = useState('a');
const [b, setB] = useState('b');


function handleClick() {
Promise.resolve().then(() => {
setA('aa');
setB('bb');
});
}


return <button onClick={handleClick}>{a}-{b}</button>
}

它会立即呈现 aa - bb吗? 还是先呈现 aa - b,然后再呈现 aa - bb

49071 次浏览

TL;DR – if the state changes are triggered asynchronously (e.g. wrapped in a promise), they will not be batched; if they are triggered directly, they will be batched.

I've set up a sandbox to try this out: https://codesandbox.io/s/402pn5l989

import React, { Fragment, useState } from 'react';
import ReactDOM from 'react-dom';


import './styles.css';


function Component() {
const [a, setA] = useState('a');
const [b, setB] = useState('b');
console.log('a', a);
console.log('b', b);


function handleClickWithPromise() {
Promise.resolve().then(() => {
setA('aa');
setB('bb');
});
}


function handleClickWithoutPromise() {
setA('aa');
setB('bb');
}


return (
<Fragment>
<button onClick={handleClickWithPromise}>
{a}-{b} with promise
</button>
<button onClick={handleClickWithoutPromise}>
{a}-{b} without promise
</button>
</Fragment>
);
}


function App() {
return <Component />;
}


const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

I've made two buttons, one triggers the state changes wrapped in a promise like in your code example, the other triggers the state changes directly.

If you look at the console, when you hit the button “with promise”, it will first show a aa and b b, then a aa and b bb.

So the answer is no, in this case, it will not render aa - bb right away, each state change triggers a new render, there is no batching.

However, when you click the button “without promise”, the console will show a aa and b bb right away.

So in this case, React does batch the state changes and does one render for both together.

Currently in React v16 and earlier, only updates inside React event handlers such as click or onChange etc are batched by default. So just like classes state updates are batched in a similar way in hooks

There is an unstable API to force batching outside of event handlers for rare cases when you need it.

ReactDOM.unstable_batchedUpdates(() => { ... })

There is a plan to batch all state updates in future version on react probably v17 or above.

Now also if the state update calls from within event handler are in async functions or triggered due to async code they won't be batched where direct updates will be batched

Where without the sync code state updates are batched and async code updates aren't

function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);


// async update from useEffect
useEffect(() => {
setTimeout(() => {
setCount1(count => count + 1);
setCount2(count => count + 2);
}, 3000);
}, []);


const handleAsyncUpdate = async () => {
await Promise.resolve("state updated");
setCount1(count => count + 2);
setCount2(count => count + 1);
};


const handleSyncUpdate = () => {
setCount1(count => count + 2);
setCount2(count => count + 1);
};


console.log("render", count1, count2);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button type="button" onClick={handleAsyncUpdate}>
Click for async update
</button>
<button type="button" onClick={handleSyncUpdate}>
Click for sync update
</button>
</div>
);
}

https://codesandbox.io/s/739rqyyqmq

If the event handler is react-based then it batches the updates. This is true for both setState or useState calls.

But it doesn't batch automatically in case the event is non-react based i.e. setTimeout, Promise calls. In short any event from Web APIs.

answer already given by @Patrick Hund .. Just Wanted to update here that with React 18 batch states update is possible for Promise, setTimeout as well by default.

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.

Check this out for detail explanation . https://github.com/reactwg/react-18/discussions/21

React 18 with createRoot, batches all updates automatically, no matter where they originate from.

Note that React 18 with legacy ReactDOM.render() keeps the old behavior. Use ReactDOM.createRoot() if you want to batch updates inside of timeouts, promises or any other event.

Here we update state twice inside of a timeout, but React renders only once:

import React, { useState } from "react";
import ReactDOM from "react-dom";


function App() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);


function handleClick() {
setTimeout(() => {
setX((p) => p + 1);
setY((p) => p + 1);
}, 100);
}


console.log(`render x: ${x} y: ${y}`);


return (
<div className="App">
<button onClick={handleClick}>Update with promise</button>


<div>X: {x} </div>
<div>Y: {y} </div>
</div>
);
}


ReactDOM.createRoot(document.getElementById("root")).render(<App />);

This has been updated for React 18. They have introduced something called "Automatic Batching". In the earlier versions of React, batching was only done for the states triggered/updated by browser events, but with React 18 batching is done for all the states regardless of where they come from.

Consider the component:


const App = () => {
const [count, setCount] = useState(0);
const [trigger, setTreigger] = useState(false);


const handleClick = () => {
setTimeout(() => {
setCount(count => count++);
setTrigger(trigger => !trigger);
}, 100)
}
      

console.log("Re-render", count, trigger);


return (
<div>
<button onClick={handleClick}>
Click Me!
</button>
</div>);
}

Here, now with version 18, React performs batching for this case too. This shows that React is getting efficient with states regardless of where they come from.

You can verify it from the output of console.log in the above component.