What is the reasoning behind "The final argument passed to useCallback changed size between renders …" warning

huangapple go评论71阅读模式
英文:

What is the reasoning behind "The final argument passed to useCallback changed size between renders ..." warning

问题

在React文档中,建议在StackOverflow上提问关于React相关问题。我想要理解为什么会出现“传递给useCallback的最后一个参数在渲染之间的大小发生变化。此数组的顺序和大小必须保持不变。”警告的原因。

在查看React源码后,发现当prevDepsnextDeps的长度不同时,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 &lt; prevDeps.length &amp;&amp; i &lt; nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

Which means:

  • areHookInputsEqual( [&#39;a&#39;,&#39;b&#39;], [&#39;c&#39;,&#39;d&#39;] ) === false - correct
  • areHookInputsEqual( [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;], [&#39;a&#39;,&#39;b&#39;] ) === true - wrong
  • areHookInputsEqual( [&#39;a&#39;,&#39;b&#39;], [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;] ) === true - wrong
  • areHookInputsEqual( [], [&#39;a&#39;] ) === true - wrong
  • areHookInputsEqual( [&#39;a&#39;], [] ) === 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 &lt; 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(
    () =&gt; {
      return load(messageKeys)
    },
    messageKeys
  )
}

Because of current React implementation, load does not get called when messageKeys change from [] to [&#39;a&#39;].

Update (how we currently solved this)

function areArraysEqual&lt;T&gt;(prevDeps: T[], nextDeps: T[]): boolean {
  if (prevDeps === nextDeps) {
    return true
  }
  if (prevDeps.length !== nextDeps.length) {
    return false
  }
  for (let i = 0; i &lt; 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(
    () =&gt; 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[]) =&gt; {
  const [memoizedArray, setMemoizedArray] = React.useState(array);
  React.useEffect(() =&gt; {
    // 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(
    () =&gt; {
      return load(memoizedMessageKeys)
    },
    [memoizedMessageKeys]
  )
}

huangapple
  • 本文由 发表于 2023年2月18日 17:03:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75492262.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定