无法在React hooks中同时更新父值useEffect和setTimeout更新



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

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

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日(

来自React 18:中更少渲染的自动批处理

使用createRoot在React 18中启动,所有更新都将自动批处理,无论它们来自何处。

这意味着在超时、承诺、本机事件内进行更新处理程序或任何其他事件将以与内部更新相同的方式进行批处理React事件。


通过异步调用handleChangeParentValue函数,可以防止状态更新被批处理在一起。

当异步调用多个状态设置器函数时,它们不会一起被批处理。这意味着父组件将为状态设置器函数的每次调用重新呈现。

您有三个对状态设置器函数的调用,因此父组件将为每个调用重新渲染,即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>

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

解决方案

你可以把这三个独立的州合并成一个州。因此,您将只需要调用一个状态设置器函数。这样,父组件将只重新呈现一次,因为只有一个状态设置器函数调用。

更改以下

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函数中的三个状态设置函数调用替换为一个状态设置器函数调用:

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

最新更新