修复React中的 "函数使useEffect Hook的依赖关系在每次渲染时发生变化"警告

63 阅读3分钟

如果你正在使用React钩子,特别是useEffect 钩子,你可能已经遇到了以下警告。

functionName "函数使useEffect钩子(在第X行)的依赖关系在每次渲染时发生变化。把它移到useEffect的回调里面。或者,把'functionName'的定义包在它自己的useCallback()Hook中。(react-hooks/exhaustive-deps)

那么,这里发生了什么,我们该如何解决?

一个精心设计的例子

为了这篇博文的目的,让我们抛出一个快速的、伪造的例子。我们将有一个简单的count 状态。每次count ,我们想运行一个效果,将count 记录到控制台。所以我们写了下面的代码。

function App() {
  const [count, setCount] = useState(0);

  const logCount = () => {
    console.log(count);
  };

  useEffect(() => {
    logCount();
  }, [logCount]);

  return <div>{count}</div>;
}

现在我们看到了警告!现在我们有了一个简单的例子,让我们用它来探索发生了什么。

依赖性数组和参照性平等

在我们的例子中,我们的useEffect 钩子的依赖数组中有logCount 函数。这意味着每当logCount 发生变化时,该效果就会运行。

你可能会想,logCount 并没有真正改变--每次渲染都是同一个函数。但我们必须记住,JavaScript的平等性是基于参照性平等的。也就是说,只有当对象在内存中引用相同的对象时,它们才是相互平等的。这就是为什么我们最终会出现这样的情况。

console.log({ name: 'Donna' } === { name: 'Donna' });
// false

尽管有相同的键/值对,但这两个对象在内存中是分开的,因此是不平等的。

这对函数来说也是如此。

const fn1 = () => 'Donna';
const fn2 = () => 'Donna';

console.log(fn1 === fn2);
// false

就是我们的useEffect 依赖关系数组中发生的事情。logCount 函数在每次组件渲染时都会被重新创建,因此总是与前一次渲染时创建的logCount 函数不同。

我们如何解决这个问题呢?

好吧,首先我想指出,这个警告信息实际上是很有帮助的:它给了我们两个解决方案,可以帮助我们解决这个问题。

  1. logCount 移到useEffect 钩子里面
  2. logCount 裹在useCallback 钩子里

我将在这里向你展示如何实现这两个目标。

将logCount移到useEffect钩子里面

这个问题相当简单:我们把logCount 函数移到useEffect 钩子里面。由于logCount 现在是在useEffect 钩子里面,我们不再需要它在依赖关系数组中。然而,由于logCount 依赖于count 变量,我们必须将count 变量添加到依赖关系数组中。

我们固定的代码现在看起来像这样。

function App() {
  const [count, setCount] = useState(1);

  useEffect(() => {
    const logCount = () => {
      console.log(count);
    };
    logCount();
  }, [count]);

  return <div className="App">foo</div>;
}

警告已经消失了!

将logCount包裹在useCallback钩子中

你可能不希望或不能把logCount 放在useEffect 钩子里面。例如,如果你在组件的其他地方需要logCount ,把它放在效果里面可能会导致它在其他地方无法定义。

在这种情况下,解决方案是将logCount 的函数定义包裹在useCallback 钩子中。这样做的目的是返回一个记忆化的函数,这个函数的引用只有在钩子的依赖数组中的东西发生变化时才会改变。

让我们看一下正确的植入方式,然后我将详细描述发生了什么。

import { useState, useEffect, useCallback } from 'react';

function App() {
  const [count, setCount] = useState(1);

  const logCount = useCallback(() => {
    console.log(count);
  }, [count]);

  useEffect(() => {
    logCount();
  }, [logCount]);

  return <div className="App">foo</div>;
}

现在我们已经将我们的logCount 函数包裹在一个useCallback 钩子中,它将在每次渲染时保持相同的内存引用--除非其依赖数组中的东西发生变化。在这种情况下,我们将count 加入到依赖数组中。我们需要这个函数在计数更新时进行更新,否则我们的效果将无法运行。

总结

React的依赖数组问题可能相当棘手。幸运的是,总是有解决办法的。在某些情况下,你得到的警告中甚至有有用的解决方案