“useEffect 缺少依赖项”警告有时是错误的吗?

IT技术 javascript reactjs react-hooks use-effect
2021-05-07 09:00:24

我已经使用钩子一段时间了,但我从来没有完全理解为什么 React 强迫我在 useEffect 中包含一些我不想要的依赖项。

我理解 useEffect 钩子的“依赖关系”的方式

添加您想要“聆听”的值,只要它们发生变化并触发您的效果。这与简单的效果完美配合,例如:

import React, {useEffect, useState} from "react";

interface Props {
    id: string
}

const SimpleComponent = (props: Props) => {
    const {id} = props;
    const [response, setResponse] = useState<object>();
    
    useEffect(() => {
        fetch(`https://myexample/${id}`)
            .then(response => setResponse(response))
            .catch(() => console.log("An error occurs!"))
    }, [id])
    
    return <div/>
};

但是,还有一些其他情况不像上面的例子那么简单。在本例中,我们希望仅在 id 更改时触发效果:

import React, {useEffect} from "react";

interface Props {
    id: string
    callback: Function
}

const SimpleComponent = (props: Props) => {
    const {id, callback} = props;
    
    useEffect(() => {
        callback(id)
    }, [id]);

    return <div/>
};

在此示例中,我收到警告“React Hook useEffect 缺少依赖项”,它建议在依赖项数组(选项 1)中包含 'callback'或删除依赖项数组(选项 2)。

让我们来探讨一下建议:

  • 选项 1(在依赖项数组中 包含 'callback' ):在依赖项数组中包含 'callback' 将导致我的效果在 'id' 或 'callback' 更改时触发。这样做的问题是,当“回调”更改导致回调在每次渲染中都会更改时,我不想触发效果。

  • 选项 2(删除依赖项数组): 删除依赖项数组将导致我的效果在组件更改时触发,这也不是想要的行为。

发现的其他选项:

我从社区中找到了一些其他建议,但所有这些建议似乎都没有完成想要的行为。( https://stackoverflow.com/a/60327893/8168782 )

让我们快速回顾一下这些选项:

选项 1:使用空的依赖项数组:

它只会在组件挂载时触发,而不是我们想要的。

选项 2:在 useEffect() 中声明函数

在这种情况下,'callback' 是一个通过 props 传递的函数,但无论哪种方式,大多数情况下你都不能在 effect 中声明该函数,因为该函数在其他地方使用。

选项 3:使用 useCallback() 记忆

如果您将函数包装在 useCallback 中,您还需要将依赖项包含到 useCallback 依赖项数组中,这将导致每次依赖项更改时再次触发 useCallback,因此也会触发 useEffect。

选项 4:禁用 eslint 的警告

没有考虑,因为我试图理解问题而不是简单地忽略它。

我真的对这个警告感到困惑,我不知道是否在某些情况下警告是错误的并且应该被忽略(似乎是错误的)或者我遗漏了什么。

1个回答

我个人总是禁用该 eslint 规则。由于 eslint 无法理解你的逻辑内涵,它只能彻底检查闭包中捕获的所有变量,并警告你从 dep-list 中丢失的变量。但是很多时候它是矫枉过正的,就像在您的用例中一样。这就是我选择的理由。

如果您对useEffect工作原理有清楚的了解,禁用此规则不会造成太大的痛苦。我个人不记得经历过。

第二种解决方案是保留规则,但要解决它。我为你准备了一个useFn自定义钩子:

function useFn(fn) {
  const ref = useRef(fn);
  ref.current = fn;

  function wrapper() {
    return ref.current.apply(this, arguments)
  }

  return useRef(wrapper).current
}

这个钩子返回一个稳定的wrapper函数引用,它只是一个调用实际的代理fn,但在重新渲染时不会改变。

const SimpleComponent = (props: Props) => {
    const {id, callback: _callback} = props;

    const callback = useFn(_callback)
    
    useEffect(() => {
        callback(id)
    }, [id, callback]);

    return <div/>
};

现在你满足了 eslint 规则,同时你不会触发不必要的useEffect重新运行。


作为题外话。我还使用useFn钩子来包装传递给子组件props的函数。

传递箭头函数是 React 中大量使用的模式。有时您有一个重新渲染的组件,您React.memo(Component)将其包装起来,然后传递一个<Component onClick={e => { ... }} />内联函数,这有效地使 memoize 效果无效。useFn来救援:

<Component onClick={useFn(e => { ... })} />