首页 > 解决方案 > React useState 重新渲染触发器的行为取决于其调用时间

问题描述

假设你有一个有多个 useState 的钩子,例如:

function useMyHook() {
  const [s1, setS1] = useState();
  const [s2, setS2] = useState();
  const setFn = useCallback(
    () => {
      console.log('set s1');
      setS1(Date.now());
      console.log('set s2');
      setS2(Date.now());
    },
    [setS1, setS2]
  );
  return setFn;
}

现在,根据您调用的时间,setFn反应将触发一个或两个重新渲染。

function MyComponent() {
  console.log('render')
  const setTwoStates = useMyHook()

  // sync call => triggers only one re render
  useEffect(() => setTwoStates(), [])
  // console.logs are: render, set s1, set s2, render

  return (
    <div>hello</div>
  );
}

function MyComponent() {
  console.log('render')
  const setTwoStates = useMyHook()

  // as soon as the setFn is called in a later tick, react triggers two re renders
  useEffect(() => setTimeout(setTwoStates, 1), [])
  // console.logs are: render, set s1, render, set s2, render 

  return (
    <div>hello</div>
  );
}

有人对此有解释吗?这可能会导致意外行为,具体取决于您调用钩子的方式。

我还创建了一个小的示例存储库,以防你想玩这个

https://github.com/JohannesMerz/react-setstate-rerenders

标签: reactjs

解决方案


当 React 在 useEffect 回调或事件处理程序(函数正在运行时)中设置状态时,React 将批处理状态设置器,但在您的异步示例中,状态是在效果函数返回后设置的。

const syncFn = () => 
  console.log('in sync function');
const asyncFn = () => setTimeout(
  ()=>console.log('in async function'),
  10
);
syncFn();
console.log('sync function returned');
asyncFn();
console.log('async function returned');

您可以在该片段中看到in async function日志 async function returned因此任何设置状态都将在效果回调或事件处理程序返回发生,并且除非您明确告诉 React 对其进行批处理,否则无法对其进行批处理。

您可以使用unstable_batchedUpdates来告诉 react 批量更新:

const App = () => {
  const [item1, setItem1] = React.useState(0);
  const [item2, setItem2] = React.useState(0);
  const render = React.useRef(0);
  //mutate render ref (shows how often component is rendered)
  render.current++;
  const asyncUpdates = React.useCallback(() => {
    Promise.resolve().then(() => {
      ReactDOM.unstable_batchedUpdates(() => {
        setItem1((item) => item + 1);
        setItem2((item) => item + 1);
      });
    });
  }, []);
  const syncUpdates = React.useCallback(() => {
    setItem1((item) => item + 1);
    setItem2((item) => item + 1);
  }, []);
  const asyncNonBatchedUpdates = React.useCallback(() => {
    Promise.resolve().then(() => {
      setItem1((item) => item + 1);
      setItem2((item) => item + 1);
    });
  }, []);
  //using asyncUpdates in effect on mount
  React.useEffect(asyncUpdates, []);
  return (
    <div>
      <h1>Rendered: {render.current}</h1>
      <div>
        <div>item1:{item1}</div>
        <div>item2:{item2}</div>
      </div>
      {/* run async updates as event handler */}
      <button onClick={asyncUpdates}>async updates</button>
      <button onClick={syncUpdates}>sync updates</button>
      <button onClick={asyncNonBatchedUpdates}>
        async non bathed updates
      </button>
    </div>
  );
};

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

<div id="root"></div>


推荐阅读