对 useEffect 挂钩内的多个 setState() 调用做出反应批量更新

Posted

技术标签:

【中文标题】对 useEffect 挂钩内的多个 setState() 调用做出反应批量更新【英文标题】:React batch updates for multiple setState() calls inside useEffect hook 【发布时间】:2019-11-15 00:34:57 【问题描述】:

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

Does React keep the order for state updates?

目前(React 16 及更早版本),默认情况下仅对 React 事件处理程序内部的更新进行批处理。有一个不稳定的 API 可在您需要的极少数情况下强制在事件处理程序之外进行批处理。

他还在这个 Github issue 中提到:

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

在当前版本中,如果您在 React 事件处理程序中,它们将一起批处理。React 批处理在 React 事件处理程序期间完成的所有 setState,并在退出其自己的浏览器事件处理程序之前应用它们.

但事实是,这个 sn-p 似乎证明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"/>

【问题讨论】:

【参考方案1】:

好问题。这是完成@FranklinOcean 答案的附加信息。

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

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

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

基于on the following codesandbox:


批量setStuff 调用:

同步内联组件功能块(我觉得相当于有一个效果在其他效果之前运行,没有依赖关系) 在 useEffect 块中同步 在合成事件处理程序中同步(由反应管理,例如onClick=handlerFunction

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

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

我会尝试使用未来版本的 react 重新运行沙箱,看看效果如何!

【讨论】:

【参考方案2】:

如果状态更新直接发生,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> ); 
; 

【讨论】:

【参考方案3】:

只是加入一个对我有用的“黑客”

  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”,然后在每道菜更新时触发它..

【讨论】:

以上是关于对 useEffect 挂钩内的多个 setState() 调用做出反应批量更新的主要内容,如果未能解决你的问题,请参考以下文章

在 apollo graphql 的单个 useEffect 挂钩中使用多个查询

无法对未安装的组件执行 React 状态更新(useEffect 反应挂钩)

无法使用 MaterialTable 中的 useEffect 挂钩显示数据

无法在 useEffect 挂钩中测试超时功能

如何将 setState 值插入 useEffect 挂钩?

我可以在 getStaticProps 中使用 UseEffect 挂钩吗?