如何修复使用useEffect React Hook时出现的缺失依赖警告

819

使用React 16.8.6(之前的版本16.8.3没有问题),在我试图防止一个fetch请求进入无限循环时,我会遇到这个错误:

./src/components/BusinessesList.js
Line 51:  React Hook useEffect has a missing dependency: 'fetchBusinesses'.
Either include it or remove the dependency array  react-hooks/exhaustive-deps

我一直找不到停止无限循环的解决方案。我希望避免使用useReducer()。我找到了这个讨论[ESLint] Feedback for 'exhaustive-deps' lint rule #14920,其中一个可能的解决方案是如果你认为自己知道在做什么,你可以总是 // eslint-disable-next-line react-hooks/exhaustive-deps。我对自己正在做的事情没有信心,所以我还没有尝试实现它。

我目前的设置是这样的:React hook useEffect runs continuously forever/infinite loop,唯一的评论是关于useCallback(),我不熟悉它。

我目前如何使用useEffect()(我只想在开始时运行一次,类似于componentDidMount()):

useEffect(() => {
    fetchBusinesses();
  }, []);
const fetchBusinesses = () => {
    return fetch("theURL", {method: "GET"}
    )
      .then(res => normalizeResponseErrors(res))
      .then(res => {
        return res.json();
      })
      .then(rcvdBusinesses => {
        // some stuff
      })
      .catch(err => {
        // some error handling
      });
  };

25
由于这个问题的流量很大,这里提供 Dan Abramov 的博客链接,在该博客中他详细解释了 useEffect 及其依赖项。 - chetan
7
React有一个功能请求,希望改进useEffect API以避免此问题,明确将effect触发器与effect依赖项分开:https://github.com/facebook/react/issues/22132。自从Next.js启用了默认的linting功能后,这个警告每天在世界各地出现了数百万次,这种情况必须得到解决。 - Eric Burel
8
同意,官方文档中的内容完全不清晰。像 React 这样的库不应该需要通过论坛和博客文章才能让它正常工作。 - Kokodoko
23个回答

720

如果你除了在 effect 中没有在其他地方使用 fetchBusinesses 方法,你可以将它简单地移动到 effect 中并避免警告。

useEffect(() => {
    const fetchBusinesses = () => {
       return fetch("theURL", {method: "GET"}
    )
      .then(res => normalizeResponseErrors(res))
      .then(res => {
        return res.json();
      })
      .then(rcvdBusinesses => {
        // some stuff
      })
      .catch(err => {
        // some error handling
      });
  };
  fetchBusinesses();
}, []);

然而,如果您在 effect 之外使用 fetchBusinesses,您需要注意两件事情:

  1. 当在挂载期间与其封闭闭包一起使用时,fetchBusinesses 作为方法传递是否会有任何问题?
  2. 您的方法是否依赖于从其封闭闭包接收到的某些变量?对于您来说,情况并非如此。
  3. 在每次渲染时,fetchBusinesses 将被重新创建,因此将其传递给 useEffect 将会导致问题。因此,如果您要将其传递给依赖项数组,则必须先将 fetchBusinesses 进行记忆化。

总之,如果您在 useEffect 之外使用 fetchBusinesses,您可以通过使用 // eslint-disable-next-line react-hooks/exhaustive-deps 来禁用该规则,否则您可以将该方法移至 useEffect 内部。

要禁用该规则,您应该编写:

useEffect(() => {
   // other code
   ...
 
   // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) 

71
我已经很好地使用了你概述的解决方案。另一个解决方案我在其他地方使用过,因为设置不同,那就是 useCallback()。例如: ... }, [...])``` 然后 `useEffect()` 看起来像这样: ```useEffect(() => { fetchBusinesses(); }, [fetchBusinesses]);``` - russ
6
@russ,你是正确的,如果你要将fetchBusiness传递给依赖数组,你需要使用useCallback进行记忆化。 - Shubham Khatri
49
使用 // eslint-disable-next-line react-hooks/exhaustive-deps 来告诉 linter 你的代码是正确的,就像是一种 hack。我期望他们会找到另一个解决方案,使 linter 足够聪明,能够检测出参数是否必需,而不需要使用这样的 hack。 - Olivier Boissé
3
@TapasAdhikary,是的,你可以在useEffect中使用异步函数,只需要以不同的方式编写它。请查看 https://dev59.com/k1QJ5IYBdhLWcg3wr363#53332372。 - Shubham Khatri
22
今天linter仍然很笨,如果你想在使用外部变量时获得类似于componentDidMount的行为(需要其中一些但不是全部触发重新渲染),无论你做什么都会收到警告...至少我在网上找不到解决办法。 - rayaqin
显示剩余14条评论

415

如果您正在创建新应用程序或具有足够的灵活性,则有非常好的状态管理库选项。请查看Recoil。

只是为了完整性:

1. (停止工作)将函数用作useEffect回调

useEffect(fetchBusinesses, [])

2. 在 useEffect() 内声明函数

useEffect(() => {
  function fetchBusinesses() {
    ...
  }
  fetchBusinesses()
}, [])

3. 使用 useCallback() 进行记忆化

如果你的函数中有依赖项,你需要将它们包含在 useCallback 的依赖项数组中,这样如果函数的参数发生变化,就会再次触发 useEffect。此外,这还需要大量重复代码……因此,只需像在 1. useEffect(fetchBusinesses, []) 中直接将函数传递给 useEffect 即可。

const fetchBusinesses = useCallback(() => {
  ...
}, [])
useEffect(() => {
  fetchBusinesses()
}, [fetchBusinesses])

4. 函数默认参数

根据Behnam Azimi的建议

这不是最佳实践,但在某些情况下可能会有用。

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

5. 创建自定义钩子

创建一个自定义钩子,在需要仅运行一次的函数时调用它,这样可能更加清晰。您还可以返回一个回调函数来重置“初始化”,以便在需要时重新运行。

// customHooks.js
const useInit = (callback, ...args) => {
  const [mounted, setMounted] = useState(false)
  
  const resetInit = () => setMounted(false)

  useEffect(() => {
     if(!mounted) {
        setMounted(true);
        callback(...args);
     }
  },[mounted, callback]);

  return [resetInit]
}

// Component.js
return ({ fetchBusiness, arg1, arg2, requiresRefetch }) => {
  const [resetInit] = useInit(fetchBusiness, arg1, arg2)

  useEffect(() => {
    resetInit()
  }, [requiresRefetch, resetInit]);

6. 禁用eslint的警告

禁用警告应该是你的最后手段,但如果你确实需要这么做,最好明确地在代码中注释掉,因为将来的开发人员可能会因为不知道linting被关闭而感到困惑或导致意外的错误。

useEffect(() => {
  fetchBusinesses()
}, []) // eslint-disable-line react-hooks/exhaustive-deps

4
禁用 ESLint 的警告是否可以做? - Rohan Shenoy
34
我更倾向于将禁用警告作为最后的手段,因为未来的开发人员可能会困惑或在不知道关闭linting的情况下创建意外的错误。 - jpenna
3
useEffect(fetchBusinesses, []) 会抛出错误:"TypeError: An effect function must not return anything besides a function, which is used for clean-up.",因为 fetchBusinesses 返回一个 Promise。 - Emile Bergeron
2
// eslint-disable-line react-hooks/exhaustive-deps 运行得非常好。 - Lane
2
第一个建议出奇地没有消除警告。 - ericjam
显示剩余11条评论

177
./src/components/BusinessesList.js
Line 51:  React Hook useEffect has a missing dependency: 'fetchBusinesses'.
Either include it or remove the dependency array  react-hooks/exhaustive-deps

这不是JavaScript/React错误,而是ESLint(eslint-plugin-react-hooks)的警告。

它告诉您,该hook依赖于函数fetchBusinesses,因此您应将其作为依赖项传递。

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

如果在组件中声明函数,可能会导致每次渲染时都调用该函数:

const Component = () => {
  /*...*/

  // New function declaration every render
  const fetchBusinesses = () => {
    fetch('/api/businesses/')
      .then(...)
  }

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

  /*...*/
}

因为每次函数都会重新声明一个新的引用。

正确的做法是:

const Component = () => {
  /*...*/

  // Keep the function reference
  const fetchBusinesses = useCallback(() => {
    fetch('/api/businesses/')
      .then(...)
  }, [/* Additional dependencies */])

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

  /*...*/
}

或者只需在 useEffect 中定义函数。

更多信息: [ESLint] 'exhaustive-deps' lint 规则 #14920 的反馈


2
解决方案很好,如果在函数中修改了另一个状态,则必须添加依赖项以避免出现其他意外行为。 - cesarlarsson
3
我同意这是解决摆脱代码检查器警告的一种方法,但我不理解为什么检查器会发出警告。因为你做的只是治疗一个不存在的病。依赖数组被React用来确定何时执行传递给useEffect的函数,所以在这种情况下传递fetchBusinesses可能是不必要的。如果我错了,请纠正我。 - Marcus Ekström
1
同意 - linter 警告对于 useCallbackuseMemo 非常有用,但假设你想运行 useEffect 的业务逻辑等同于每个依赖项更新的逻辑是无效的假设。例如,有许多原因你可能只想在组件加载时运行 effect,并且 useEffect(() => { ... }, []) 是实现这一点的完美工具。React/es-lint 让本不需要很难的事情变得困难了。 - Tim Trewartha
感谢您解释问题并提供解决方案,并且给出了一个很好的简单示例。 - undefined

32

这些警告非常有帮助,可以找到未能保持一致更新的组件:是否可以安全地从依赖列表中省略函数?

然而,如果您想在整个项目中删除这些警告,您可以将以下内容添加到您的 ESLint 配置中:

  {
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/exhaustive-deps": 0
    }
  }

效果非常好,谢谢您分享此解决方案,这样我就不必在我的“useEffect”中添加注释了。 - Pegues

25
React 也提供了解决方案。他们建议使用 useCallback,它将返回您的函数的备忘录版本:

'fetchBusinesses' 函数使 useEffect Hook(位于第 NN 行)的依赖项每次渲染时都会更改。为了解决这个问题,请将 'fetchBusinesses' 定义包装到其自己的 useCallback() Hook 中 react-hooks/exhaustive-deps

useCallback 的使用方法很简单,因为它与 useEffect 具有相同的签名。区别在于 useCallback 返回一个函数。 代码如下:
 const fetchBusinesses = useCallback( () => {
        return fetch("theURL", {method: "GET"}
    )
    .then(() => { /* Some stuff */ })
    .catch(() => { /* Some error handling */ })
  }, [/* deps */])
  // We have a first effect that uses fetchBusinesses
  useEffect(() => {
    // Do things and then fetchBusinesses
    fetchBusinesses();
  }, [fetchBusinesses]);
   // We can have many effects that use fetchBusinesses
  useEffect(() => {
    // Do other things and then fetchBusinesses
    fetchBusinesses();
  }, [fetchBusinesses]);

1
在我的情况下,useCallBack 钩子解决了我的问题。要详细了解,请访问文档 - khan

15
const [mount, setMount] = useState(false)
const fetchBusinesses = () => {
   // Function definition
}
useEffect(() => {
   if(!mount) {
      setMount(true);
      fetchBusinesses();
   }
},[fetchBusinesses, mount]);

这个解决方案相当简单,您不需要覆盖ESLint警告。只需维护一个标志来检查组件是否已挂载即可。


1
并且每次需要执行 componentDidMount 时,您都会这样做吗? - Zhivko Zhelev
1
这将提示您需要将mount作为useEffect的依赖项添加。 - Preston

12

只需禁用ESLint下一行即可;

useEffect(() => {
   fetchBusinesses();
// eslint-disable-next-line
}, []);

以这种方式使用它,就像组件挂载一样(只调用一次)。

更新

或者

const fetchBusinesses = useCallback(() => {
 // Your logic in here
 }, [someDeps])

useEffect(() => {
   fetchBusinesses();
// No need to skip the ESLint warning
}, [fetchBusinesses]);

每次 someDeps 改变时,fetchBusinesses 函数都将被调用。


不要禁用,只需执行此操作:[fetchBusinesses] 就会自动删除警告,这对我解决了问题。 - rotimi-best
14
@RotimiBest - 这样做会导致无限重新渲染,就像问题中描述的那样。 - user210757
2
@user210757 等等,为什么会导致无限循环呢?你并没有在从服务器获取数据后设置状态。如果你正在更新状态,只需在 useEffect 中调用函数之前编写一个 if 条件,检查状态是否为空即可。 - rotimi-best
请查看这个有用的讨论:https://github.com/facebook/create-react-app/issues/6880 - Joe
“就像组件挂载一样”是什么意思(似乎不可理解)?请通过编辑(更改)您的答案来回复,而不是在评论中回复(不要包含“编辑:”,“更新:”或类似内容 - 答案应该看起来像今天写的)。 - Peter Mortensen
显示剩余2条评论

9
这篇文章是关于使用hooks获取数据的入门指南:https://www.robinwieruch.de/react-hooks-fetch-data/ 基本上,将fetch函数定义嵌套在useEffect函数内部:
useEffect(() => {
  const fetchBusinesses = () => {
    return fetch("theUrl"...
      // ...your fetch implementation
    );
  }

  fetchBusinesses();
}, []);

7

实际上,在使用Hooks技术开发时,警告信息非常有用。但在某些情况下,这可能会令人不爽,特别是当您不需要监听依赖项更改时。

如果您不想将fetchBusinesses放在钩子的依赖项中,您可以简单地将其作为参数传递给钩子的回调函数,并将主要的fetchBusinesses设置为它的默认值,如下所示:

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

这不是最佳实践,但在某些情况下可能会有用。

此外,正如Shubham所写的那样,您可以添加以下代码告诉ESLint忽略对您的钩子进行检查。

// eslint-disable-next-line react-hooks/exhaustive-deps

为什么这不是最佳实践呢?对我来说效果很好! - undefined

7

你可以试着用这种方式:

const fetchBusinesses = () => {
    return fetch("theURL", {method: "GET"})
        .then(res => normalizeResponseErrors(res))
        .then(res => {
            return res.json();
        })
        .then(rcvdBusinesses => {
            // Some stuff
        })
        .catch(err => {
            // Some error handling
        });
  };

并且

useEffect(() => {
    fetchBusinesses();
});

它对您有效。

但我的建议是尝试这种方式,这也对您有效。 这比以前的方法更好。我是这样使用的:

useEffect(() => {
    const fetchBusinesses = () => {
        return fetch("theURL", {method: "GET"})
            .then(res => normalizeResponseErrors(res))
            .then(res => {
                return res.json();
            })
            .then(rcvdBusinesses => {
                // Some stuff
            })
            .catch(err => {
                // Some error handling
            });
    };

    fetchBusinesses();
}, []);

如果你基于特定的id获取数据,在回调函数中使用[id] 来添加useEffect钩子,那么就不会显示以下警告信息: React Hook useEffect has a missing dependency: 'anything'. Either include it or remove the dependency array

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接