在 useEffect 挂钩中对多个 setState() 调用进行批处理更新

React batch updates for multiple setState() calls inside useEffect hook

关于 Dan Abramov 在 SO 上的这个回答,我发现了以下内容:

Currently (React 16 and earlier), only updates inside React event handlers are batched by default. There is an unstable API to force batching outside of event handlers for rare cases when you need it.

他还在这个 Github 问题中提到:

https://github.com/facebook/react/issues/10231#issuecomment-316644950

In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.

但事实是,此片段似乎证明 useEffect() 内的多个 setState 调用的更新是分批处理的。

问题

React 是否也总是对 useEffect 中的多个 setState() 调用进行批量更新?它在其他什么地方这样做?

注意:根据他的回答,在下一个主要版本(可能是 v17)中,默认情况下 React 将在任何地方进行批处理。

SNIPPET: useEffect() 内的批量更新,具有多个 setState() 调用

function App() {

  console.log('Rendering app...');
  
  const [myState,setMyState] = React.useState(0);
  const [booleanState, setBooleanState] = React.useState(false);
  
  console.log('myState: ' + myState);
  console.log('booleanState: ' + booleanState);
  
  React.useEffect(()=>{
    console.log('Inside useEffect...');
    setMyState(1);
    setMyState((prevState) => prevState +1);
    setMyState(3);
    setMyState(4);
    setMyState(5);
    setBooleanState(true);
  },[]);

  return(
    <div>App - Check out my console!</div>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

如果状态更新直接发生,React 会分批更新。

已批处理:

export default () => { 
   const [a, setA] = React.useState(0); 
   const [b, setB] = React.useState(0);

   useEffect(() => {
    setA(1); setB(1);
   },[]);

   return (  
    <div> 
      <p>A: {a}</p> 
      <p>B: {b}</p> 
    </div> ); 
};

单击此按钮只会重新渲染组件一次。

非批处理:

export default () => { 
  const [a, setA] = React.useState(0); 
  const [b, setB] = React.useState(0);

   useEffect(() => { 
    setTimeout(() => { setA(1); setB(1); }, 1000); }
   , []);

   return ( 
    <div> 
     <p>A: {a}</p> 
     <p>B: {b}</p> 
    </div> ); 
}; 

问得好。这是完成@FranklinOcean 回答的附加信息。

2021 年更新:React 18 即将发生的变化

请参阅 React 18 中有关此主题的 Dan Abramov 更新,它添加了自动批处理:https://github.com/reactwg/react-18/discussions/21

回答当前版本的反应,即 17.0.2 及以下,截至 2021 年。

基于on the following codesandbox:


批量 setStuff 调用:

  • 同步内联组件功能块(我认为相当于在其他效果之前运行ning并且没有依赖)
  • 在一个 useEffect 同步块中
  • 在合成事件处理程序中同步(由 react 管理,例如 onClick={handlerFunction}

每次都会触发重新渲染的非批处理调用:

  • 任何异步代码(promise/async 上述任何用例中的函数)
  • 非合成事件(在反应库之外管理的事件)
  • 包括 XHR 或其他网络回调

我会尝试重新运行沙箱与未来版本的反应,看看它如何!

只是想加入一个对我有用的“hack”

  const [dishes, dishesSet] = React.useState(state)
  const [isConnectedToDB, isConnectedToDBSet] = React.useState(false)

  React.useEffect(() => {
    const databaseDishesRef = firebase.database().ref(process.env.FIREBASE_DISHES_REF)
    databaseDishesRef.on('value', (snapshot) => {
      const value = snapshot.val()
      // isConnectedToDBSet(true) // this was not working
      dishesSet(() => {
        isConnectedToDBSet(true) // had to move it in here to batch the state updates

        return Object.entries(value).map(([, dish]) => dish)
      })
    })

    return () => {
      databaseDishesRef.off('value')
    }
  }, [])

另一种方法是编写一个“Reac.useEffectfor theisConnectedToDB”,然后在每次更新菜肴时触发它..