我目前正在尝试在重新渲染组件或特别是重新创建(回调)函数时了解 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])} />;
}
该示例包含一个带有状态的父 (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
。这“解决”了重新渲染,对于我的用例来说是绝对可以接受的。但是,据我所知,这是不好的做法,因为 onChange
在效果中使用。此外,ESLint 将此表示为警告。useCallback
也没有帮助,只是“冒泡”重新创建 onChange
函数。
所以我的问题是:React 状态(包含数组)更新的处理方式是否不同,并且忽略此处的依赖项是否有效?执行此操作的正确方法是什么?
答案 0 :(得分:1)
为什么要更改 onChange?
在每次渲染时,您创建一个新的匿名函数 (v) => setState([v])
。
由于 React makes 在渲染之前与之前的 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]), []);
请注意,我们记住该函数只是因为其非平凡用例(注意过早优化)。