英文:
What is the reasoning behind "The final argument passed to useCallback changed size between renders ..." warning
问题
在React文档中,建议在StackOverflow上提问关于React相关问题。我想要理解为什么会出现“传递给useCallback
的最后一个参数在渲染之间的大小发生变化。此数组的顺序和大小必须保持不变。”警告的原因。
在查看React源码后,发现当prevDeps
和nextDeps
的长度不同时,React似乎没有正确比较它们的数组。
比较函数如下(为简洁起见省略了一些检查):
function areHookInputsEqual(prevDeps, nextDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
这意味着:
areHookInputsEqual(['a', 'b'], ['c', 'd']) === false
- 正确areHookInputsEqual(['a', 'b', 'c'], ['a', 'b']) === true
- 错误areHookInputsEqual(['a', 'b'], ['a', 'b', 'c']) === true
- 错误areHookInputsEqual([], ['a']) === true
- 错误areHookInputsEqual(['a'], []) === true
- 错误
为什么不将该函数编写成以下方式,以从代码库中删除警告?
function areHookInputsEqual(prevDeps, nextDeps) {
if (prevDeps.length !== nextDeps.length) {
return false;
}
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
引发这个问题的用例
我们有一个自定义钩子如下:
function useLoadMessageKeys(messageKeys: string[]) {
return React.useCallback(
() => {
return load(messageKeys);
},
messageKeys
);
}
由于当前的React实现,当messageKeys
从[]
变为['a']
时,load
不会被调用。
更新(我们目前是如何解决这个问题的)
function areArraysEqual<T>(prevDeps: T[], nextDeps: T[]): boolean {
if (prevDeps === nextDeps) {
return true;
}
if (prevDeps.length !== nextDeps.length) {
return false;
}
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false;
}
}
return true;
}
export function useLoadMessageKeys(messageKeys: string[]) {
const messageKeysRef = React.useRef(messageKeys);
if (!areArraysEqual(messageKeys, messageKeysRef.current)) {
messageKeysRef.current = messageKeys;
}
const currentMessageKeys = messageKeysRef.current;
return React.useCallback(
() => load(currentMessageKeys),
[currentMessageKeys]
);
}
如果在问题中链接的代码正确比较了这两个数组,我们将避免出现这种复杂性。
英文:
Opening this question here, because StackOverflow is listed as a recommended place for asking React-related questions in React docs.
I am looking for a reasoning behind throwing a The final argument passed to useCallback changed size between renders. The order and size of this array must remain constant.
warning.
After looking into React code it looks like React does not properly compare prevDeps
and nextDeps
arrays when they have different lengths.
The comparison function looks like this (some checks were omitted for brevity):
function areHookInputsEqual(prevDeps,nextDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
Which means:
areHookInputsEqual( ['a','b'], ['c','d'] ) === false
- correctareHookInputsEqual( ['a','b','c'], ['a','b'] ) === true
- wrongareHookInputsEqual( ['a','b'], ['a','b','c'] ) === true
- wrongareHookInputsEqual( [], ['a'] ) === true
- wrongareHookInputsEqual( ['a'], [] ) === true
- wrong
Why not to write this function as following and remove warning from the codebase?
function areHookInputsEqual(prevDeps,nextDeps) {
if (prevDeps.length !== nextDeps.length) {
return false;
}
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
The use case which led to this question
We have a custom hook like this:
function useLoadMessageKeys(messageKeys: string[]) {
return React.useCallback(
() => {
return load(messageKeys)
},
messageKeys
)
}
Because of current React implementation, load
does not get called when messageKeys
change from []
to ['a']
.
Update (how we currently solved this)
function areArraysEqual<T>(prevDeps: T[], nextDeps: T[]): boolean {
if (prevDeps === nextDeps) {
return true
}
if (prevDeps.length !== nextDeps.length) {
return false
}
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}
export function useLoadMessageKeys(messageKeys: string[]) {
const messageKeysRef = React.useRef(messageKeys)
if (!areArraysEqual(messageKeys, messageKeysRef.current)) {
messageKeysRef.current = messageKeys
}
const currentMessageKeys = messageKeysRef.current
return React.useCallback(
() => load(currentMessageKeys),
[currentMessageKeys]
)
}
If the code linked in the question properly compared 2 arrays we'd avoid having this complexity.
答案1
得分: 2
I think the reasoning is that the array of dependencies should always be exactly the list of variables used inside the effect. So in particular it can be statically determined and cannot change size. If it does change size, you're probably doing something more than just listing the dependencies, so it is warning you that you are not using the dependency list as intended.
You could use instead use a version of messageKeys
that does not change if it is only shallow equal to the previous one (untested):
const useMemoizedArray = (array: string[]) => {
const [memoizedArray, setMemoizedArray] = React.useState(array);
React.useEffect(() => {
// Define `isShallowEqual` yourself somewhere
if (!isShallowEqual(array, memoizedArray)) {
setMemoizedArray(array);
}
}, [array, memoizedArray]);
return memoizedArray;
};
function useLoadMessageKeys(messageKeys: string[]) {
const memoizedMessageKeys = useMemoizedArray(messageKeys);
return React.useCallback(
() => {
return load(memoizedMessageKeys)
},
[memoizedMessageKeys]
)
}
英文:
I think the reasoning is that the array of dependencies should always be exactly the list of variables used inside the effect. So in particular it can be statically determined and cannot change size. If it does change size, you're probably doing something more than just listing the dependencies, so it is warning you that you are not using the dependency list as intended.
You could use instead use a version of messageKeys
that does not change if it is only shallow equal to the previous one (untested):
const useMemoizedArray = (array: string[]) => {
const [memoizedArray, setMemoizedArray] = React.useState(array);
React.useEffect(() => {
// Define `isShallowEqual` yourself somewhere
if (!isShallowEqual(array, memoizedArray)) {
setMemoizedArray(array);
}
}, [array, memoizedArray]);
return memoizedArray;
};
function useLoadMessageKeys(messageKeys: string[]) {
const memoizedMessageKeys = useMemoizedArray(messageKeys);
return React.useCallback(
() => {
return load(memoizedMessageKeys)
},
[memoizedMessageKeys]
)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论