arrays - 反应重新渲染循环
问题描述
我目前正在尝试了解 React 在重新渲染组件时或特别是在重新创建(回调)函数时的内部工作原理。
在这样做的过程中,我遇到了一个我无法理解的现象。它(仅)在具有包含数组的状态时发生。这是显示“问题”的最小代码:
import { useEffect, useState } from "react";
export function Child({ value, onChange }) {
const [internalValue, setInternalValue] = useState(value);
// ... stuff interacting with internalValue
useEffect(() => {
onChange(internalValue);
}, [onChange, internalValue]);
return <div>{value}</div>;
}
export default function App() {
const [state, setState] = useState([9.0]);
return <Child value={state[0]} onChange={(v) => setState([v])} />;
}
该示例包含一个具有状态的 Parent ( App
) 组件,该状态是单个数字的数组,该数组被赋予该Child
组件。Child 可能会做一些内部工作并使用 设置内部状态setInternalValue
,这反过来会触发效果。此效果将引发 onChange 函数,更新父状态数组的值。(请注意,此示例已最小化以显示效果。数组将具有多个值,其中每个子组件都显示)但是此示例导致子组件无休止地重新渲染,并在整个过程中引发以下控制台警告:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
调试显示,重新渲染是由于onChange
被更改而发生的。但是,我不明白这一点。为什么onChange
被改变?internalState 和 state 都不会在任何地方更改。
我发现了两种解决方法:
- 从
onChange
Child 中效果的依赖项中删除。这“解决”了重新渲染,对于我的用例来说绝对可以接受。但是,据我所知,这是不好的做法,因为onChange
在效果内使用。此外,ESLint 将此指示为警告。 - 在状态中使用“原始”数字,而不是数组。这也将摆脱重新渲染。然而,这仅在这个最小示例中是可接受的,因为只使用了一个数字。对于数字的动态计数,此解决方法不可行。
useCallback
也无济于事,只是“冒泡”了功能的重新创建onChange
。
所以我的问题是:React 状态(包括数组)更新的处理方式是否不同,并且忽略了此处有效的依赖项?这样做的正确方法是什么?
解决方案
为什么要更改 onChange?
在每次渲染时,您都会创建一个新的匿名函数 (v) => setState([v])
。
由于React 在渲染之前与之前的 props 进行了浅层比较,因此它总是会导致渲染,因为在 Javascript 中:
const y = () => {}
const x = () => {}
x !== y // always true
// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])
onChangeFromPreviousRender !== onChangeInCurrentRender
这样做的正确方法是什么?
有两种方法可以纠正它,因为setState
保证是稳定的,你可以只传递 setter 并在组件本身中使用你的逻辑:
// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />
useEffect(() => {
// set as array
onChange([internalValue]);
}, [onChange, internalValue]);
或者,记忆函数将保证相同的身份。
const onChange = useCallback(v => setState([v]), []);
请注意,我们记住该函数只是因为它的用例不平凡(提防过早优化)。
推荐阅读
- osgi - OSGI 捆绑包版本 (Apache Felix)
- javascript - 无法在生产模式下启动 sapper 应用程序
- android - 活动之间的过渡
- azure-devops - 在远程服务器上创建目录后 Azure DevOps 管道上传到 FTP
- java - Android 中的地图视图
- file - 读取和查找之间的区别(随机访问)
- clojure - 避免输出clojure函数
- jupyter-lab - 请使用不同的工作区。此工作区已在另一个 JupyterLab 窗口中使用。请输入另一个工作区名称
- java - 列表数据包含其他列表中的所有确切数据
- r - ggplot:在时间序列数据中添加标签的奇怪问题