无法在 React 挂钩 useEffect 和 setTimeout 更新中一起更新父值
cannot update parent value all together in React hooks useEffect with setTimeout update
我有两个组件,parentComponent
和 ChildrenComponent
。当我单击该按钮时,它将呈现 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
});
};
我有两个组件,parentComponent
和 ChildrenComponent
。当我单击该按钮时,它将呈现 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
});
};