[英]Obeying react-hooks/exhaustive-deps leads to infinite loops and/or lots of useCallback()
My app has a userService
that exposes a useUserService
hook with API functions such as getUser
, getUsers
, etc. I use a Hook for this because the API calls require information from my Session state, which is provided by a React Context Provider. My app has a
userService
that exposes a useUserService
hook with API functions such as getUser
, getUsers
, etc. I use a Hook for this because the API calls require information from my Session state, which is provided by a React Context Provider.
Providing the getUsers
functions to a useEffect
call makes the react-hooks/exhaustive-deps
eslint rule unhappy, because it wants the getUsers
function listed as a dep - however, listing it as a dep causes the effect to run in an infinite loop, because each time the component is re-rendered, it re-runs the useUserService
hook, which recreates the getUsers
function.将
getUsers
函数提供给useEffect
调用会使react-hooks/exhaustive-deps
eslint 规则不满意,因为它希望将getUsers
function 列为 dep - 但是,将其列为 dep 会导致效果在无限循环中运行,因为每次重新渲染组件时,它都会重新运行useUserService
挂钩,从而重新创建getUsers
function。
This can be remedied by wrapping the functions in useCallback
, but then the useCallback
dependency array runs into a similar lint rule.这可以通过将函数包装在
useCallback
中来解决,但是useCallback
依赖数组会遇到类似的 lint 规则。 I figure I must be doing something wrong here, because I can't imagine I'm supposed to wrap every single one of these functions in useCallback()
.我想我一定是在这里做错了,因为我无法想象我应该将这些函数中的每一个都包装在
useCallback()
中。
I've recreated the issue in Codesandbox.我在 Codesandbox 中重新创建了这个问题。
1: Encounter eslint
warning: https://codesandbox.io/s/usecallback-lint-part-1-76bcf?file=/src/useFetch.ts 1:遇到
eslint
警告: https://codesandbox.io/s/usecallback-lint-part-1-76bcf?file=/src/useFetch.ts
2: Remedy eslint
warning by sprinkling useCallback
in, leading to another eslint
warning: https://codesandbox.io/s/usecallback-lint-part-2-uwhhf?file=/src/App.js 2:通过撒上
useCallback
来解决eslint
警告,导致另一个eslint
警告: https://codesandbox.io/s/usecallback-lint-part-2-uwhhf?file=/src/App.js
3: Remedy that eslint
rule by going deeper: https://codesandbox.io/s/usecallback-lint-part-3-6wfwj?file=/src/apiService.ts 3:通过更深入地解决
eslint
规则: https://codesandbox.io/s/usecallback-lint-part-3-6wfwj?file=/src/apiService.ts
Everything works completely fine if I just ignore the lint warning... but should I?如果我只是忽略 lint 警告,一切都会正常工作......但我应该吗?
If you want to keep the exact API and constraints you've already chosen, that is the canonical solution — you need to ensure that everything "all the way down" has useCallback
or useMemo
around it.如果你想保持准确的 API 和你已经选择的约束,那是规范的解决方案——你需要确保所有“一路下来”的东西都有
useCallback
或useMemo
。
So this is unfortunately correct:所以不幸的是,这是正确的:
I can't imagine I'm supposed to wrap every single one of these functions in useCallback()
我无法想象我应该将这些函数中的每一个都包装在 useCallback()
There is a concrete practical reason for it.它有一个具体的实际原因。 If something at the very bottom of the chain changes (in your example, it would be the "session state from React's context" you're referring to), you somehow need this change to propagate to the effects.
如果链最底部的某些内容发生了变化(在您的示例中,它将是您所指的“来自 React 上下文的会话 state”),您需要以某种方式将此更改传播到效果。 Since otherwise they'd keep using stale context.
否则他们会继续使用陈旧的上下文。
However, my general suggestion would be to try to avoid building APIs like this in the first place .但是,我的一般建议是首先尽量避免构建这样的 API 。 In particular, building an API like
useFetch
that takes an arbitrary function as a callback, and then calling it in effect, poses all sorts of questions.特别是,构建一个像
useFetch
这样的 API 将任意 function 作为回调,然后有效地调用它,提出了各种各样的问题。 Like what do you want to happen if that function closes over some state that changes?比如,如果 function 关闭了一些发生变化的 state,你想发生什么? What if the consumer passes an inline function that's always "different"?
如果消费者传递一个总是“不同”的内联 function 怎么办? If you only respected the initial function, would you be OK with the code being buggy when you're getting
cond? fn1: fn2
如果您只尊重最初的 function,那么当您得到
cond? fn1: fn2
cond? fn1: fn2
as an argument? cond? fn1: fn2
作为参数?
So, generally speaking, I would strongly discourage building a helper like this, and instead rethink the API so that you don't need to inject a "way to fetch" into a data fetching function, and instead that data fetching function knows how to fetch by itself. So, generally speaking, I would strongly discourage building a helper like this, and instead rethink the API so that you don't need to inject a "way to fetch" into a data fetching function, and instead that data fetching function knows how to自己取。
TLDR: a custom Hook taking a function that is then needed inside of an effect is often unnecessarily generic and leads to complications like this. TLDR:采用 function 的自定义 Hook,然后在效果内部需要它通常是不必要的通用,并导致这样的并发症。
For a concrete example of how you could write this differently:有关如何以不同方式编写此代码的具体示例:
// api.js
export function fetchUser(session, userId) {
return axios(...)
}
function useSession() {
return useContext(Session)
}
function useFetch(callApi) {
const session = useSession()
useEffect(() => {
callApi(session).then(...)
// ...
}, [callApi, session])
}
And和
import * as api from './api'
function MyComponent() {
const data = useFetch(api.fetchUser)
}
Here, api.fetchUser
never "changes" so you don't have any useCallback
at all.在这里,
api.fetchUser
永远不会“改变”,所以你根本没有任何useCallback
。
Edit: I realized I skipped passing the arguments through, like userId
.编辑:我意识到我跳过了通过 arguments ,比如
userId
。 You could add an args
array to useFetch
that only takes serializable values, and use JSON.stringify(args)
in your dependencies.您可以将
args
数组添加到仅采用可序列化值的useFetch
,并在您的依赖项中使用JSON.stringify(args)
。 You'd still have to disable the rule but crucially you're complying with the spirit of the rule — dependencies are specified .您仍然必须禁用该规则,但至关重要的是您要遵守该规则的精神 - 指定了依赖项。 Which is pretty different from disabling it for functions, which leads to subtle bugs.
这与为功能禁用它有很大不同,后者会导致细微的错误。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.