"useEffect has a missing dependency" 警告有时是错误的吗?

Is the "useEffect has a missing dependency" warning sometimes wrong?

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

我理解 'dependencies' useEffect 钩子的方式

添加您想要的值 'listen' 每当它们发生变化并触发您的效果时。这与像这样的简单效果完美搭配:

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 缺少依赖项”,它建议在依赖项数组中包含 'callback'( 选项 1)或删除依赖项数组(选项 2)。

我们来探讨一下建议:

找到其他选项:

我从社区找到了一些其他建议,但所有这些建议似乎都没有实现所需的行为。 ()

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

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

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

选项2:在useEffect()

中声明函数

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

选项 3:使用 useCallback() 进行记忆

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

选项 4:禁用 eslint 的警告

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

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

我个人总是禁用那个 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
}

此挂钩 returns 是 wrapper 函数的稳定引用,它只是调用实际 fn 的代理,但不会在 re-rendering 中发生变化。

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

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

    return <div/>
};

现在您满足了 eslint 规则,同时您不会触发不需要的 useEffect re-run.


作为 off-topic 旁注。我还使用 useFn 钩子来包装传递给子组件道具的函数。

传递箭头函数是 React 中大量使用的模式。有时你有一个组件 re-render,你 React.memo(Component) 包装它,然后你传递一个 <Component onClick={e => { ... }} /> 内联函数,这有效地使 memoize 效果无效。 useFn 前来救援:

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