无法在 React 挂钩 useEffect 和 setTimeout 更新中一起更新父值

cannot update parent value all together in React hooks useEffect with setTimeout update

我有两个组件,parentComponentChildrenComponent。当我单击该按钮时,它将呈现 handleClick 方法来更新 ChildrenComponent 中的 childrenValue。在 ChildrenComponent 里面,我用 useEffect 来观察 childrenValue 的变化。

当我在 useEffect 中有一个计时器时,而不是在 ParentComponent 上重新渲染一次,为什么每次父组件值更改时它都会渲染?

父组件:

import React, { useState } from "react";
import { ChildrenComponent } from "./ChildrenComponent";

export const ParentComponent = () => {
  const [test1, setTest1] = useState(false);
  const [test2, setTest2] = useState(false);
  const [test3, setTest3] = useState(false);

  console.log("ParentComponent render", test1, test2, test3);

  const handleChangeParentValue = () => {
    setTest1(true);
    setTest2(true);
    setTest3(true);
  };

  return (
    <>
      <ChildrenComponent handleChangeParentValue={handleChangeParentValue} />
    </>
  );
};

子组件:

import React, { useState, useEffect } from "react";

export const ChildrenComponent = (props) => {
  const { handleChangeParentValue } = props;
  const [childrenValue, setChildrenValue] = useState(false);

  useEffect(() => {
    if (childrenValue) {
      // handleChangeParentValue(); 
      // if I use handleChangeParentValue() instead of a timer, 
      // the parent component only render once with three true value: 
      // ParentComponent render true true true
      const timerSuccess = setTimeout(() => {
        handleChangeParentValue();
      }, 1000);
      return () => {
        clearTimeout(timerSuccess);
      };
    }
  }, [childrenValue]);

  const handleClick = () => {
    setChildrenValue((prev) => !prev);
  };
  return (
    <>
      <div>ChildrenComponent</div>
      <button onClick={handleClick}>CLick me</button>
    </>
  );
};

当我使用计时器时,父级会在每次 setTest 时渲染 运行:

ParentComponent render true false false
ParentComponent render true true false
ParentComponent render true true true

为什么会这样?如果我想在子组件上使用计时器进行值延迟更新,但仍然希望父组件渲染一次或同时更新父值而不是单独更新,我该怎么办?

更新(2021 年 7 月 15 日)

来自Automatic batching for fewer renders in React 18:

Starting in React 18 with createRoot, all updates will be automatically batched, no matter where they originate from.

This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events.


您正在通过异步调用 handleChangeParentValue 函数来防止状态更新被一起批处理。

当多个状态 setter 函数被 异步调用 时,它们 未批处理 在一起。这意味着父组件将在每次调用 state setter 函数时重新渲染。

您调用了 3 次状态 setter 函数,因此父组件将在每次调用时重新渲染,即 3 次。

调用handleChangeParentValue同步导致多个状态更新被批处理在一起,导致只有一次重新渲染父组件。

以下演示展示了此行为的实际效果。

function App() {
  const [state1, setState1] = React.useState({});
  const [state2, setState2] = React.useState({});
  const [state3, setState3] = React.useState({});

  console.log("App component rendered");
  
  const asyncStateUpdate = () => {
    console.log("triggering state updates asynchronously not batched");
    
    setTimeout(() => {
      setState1({});
      setState2({});
      setState3({});
    }, 1000);
  };
  
  const syncStateUpdate = () => {
     console.log("triggering state updates synchronously - batched");
     setState1({});
     setState2({});
     setState3({});
  };
  
  return (
    <div>
      <button onClick={asyncStateUpdate}>Trigger State Update Asynchronously</button>
      <button onClick={syncStateUpdate}>Trigger State Update Synchronously</button>
    </div>
  );
}

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

注意:状态更新本身是异步的,但您必须同步触发状态更新才能将多个状态更新一起批处理。

解决方案

您可以将三个独立的状态合并为一个状态。因此,您只需调用一个状态 setter 函数。这样父组件只会重新渲染一次,因为只有一个状态 setter 函数调用。

更改以下内容

const [test1, setTest1] = useState(false);
const [test2, setTest2] = useState(false);
const [test3, setTest3] = useState(false);

const [state, setState] = useState({
  test1: false,
  test2: false,
  test3: false
});

并将 handleChangeParentValue 函数中的三个状态 setter 函数调用替换为一个状态 setter 函数调用:

const handleChangeParentValue = () => {
   setState({
      test1: true,
      test2: true,
      test3: true
   });
};