理解React Hooks中的'exhaustive-deps' lint规则

174

我很难理解“exhaustive-deps”代码检查规则。

我已经阅读了这篇文章这篇文章,但我没有找到答案。

这是一个带有代码检查问题的简单React组件:

const MyCustomComponent = ({onChange}) => {
    const [value, setValue] = useState('');

    useEffect(() => {
        onChange(value);
    }, [value]);

    return (
        <input 
           value={value} 
           type='text' 
           onChange={(event) => setValue(event.target.value)}>
        </input>
    )
} 

需要我将 onChange 添加到 useEffect 的依赖数组中。但是据我理解,onChange 永远不会改变,所以它不应该在那里。

通常我会这样管理:

const MyCustomComponent = ({onChange}) => {
    const [value, setValue] = useState('');

    const handleChange = (event) => {
        setValue(event.target.value);
        onChange(event.target.value)
    }

    return (
        <input 
           value={value} 
           type='text'
           onChange={handleChange}>
        </input> ​
    )
} 

为什么需要lint?对于第一个示例的lint规则有清晰的解释吗?

或者我不应该在这里使用useEffect吗?(我是钩子的新手)


1
是的,这里没有使用effect的理由,useEffect非常类似于componentWillMountcomponentDidMount的组合,当你从useEffect返回一个函数时,该函数被视为componentWillUnmount。目前你要处理的只是一个简单的状态改变,useState钩子已足以实现这一点。 - Mike Abeln
onChange 不会改变,但是 value 会改变。 - Dave Newton
1
@MikeAbeln,它们不仅改变状态,还调用作为属性传递的点击处理程序。 - Dave Newton
2
@DaveNewton 很好的发现,我没注意到。但是,useEffect 似乎不太合适。属性 onChange 可以很容易地移动到 inputonChange 方法的主体中。尽管为了清晰起见应该将其重命名。基本上是问题中 OP 给出的第二个示例。 - Mike Abeln
{onChange} 是一个回调函数,用于将输入值的变化更新到父组件中。(在这个例子中) - Logan Wlv
我已经在 https://github.com/facebook/react/issues/22132 上提出了一个功能请求,并在 https://codesandbox.io/s/fancy-sea-zj5e4 上展示了各种方法,我想听听你的意见。我认为 useEffect 可以从额外的 triggers 参数中受益。 - Eric Burel
2个回答

150

代码检查规则要求将 onChange 放入 useEffect 钩子中的原因是因为在渲染之间,onChange 可能会发生变化,而该检查规则旨在防止这种“过时数据”引用。

例如:

const MyParentComponent = () => {
    const onChange = (value) => { console.log(value); }

    return <MyCustomComponent onChange={onChange} />
}

每次渲染MyParentComponent时,都会向MyCustomComponent传递不同的onChange函数。

在你的特定情况下,你可能不关心这一点:你只想在值改变时调用onChange,而不是在onChange函数改变时调用。useEffect的使用方式并没有清晰地反映出这一点。


根本问题在于你的useEffect有些不符合惯例。

useEffect最好用于副作用,但在这里你将其用作一种"订阅"概念,比如:"在Y改变时执行X"。这在功能上确实可以工作,由于deps数组机制的原因,(尽管在这种情况下,你也会在初始渲染时调用onChange,这可能是不必要的),但这不是预期的目的。

在这里调用onChange确实不是一种副作用,它只是触发<input>onChange事件的效果。因此,我认为同时调用onChangesetValue的第二个版本更符合惯例。

如果有其他设置值的方法(例如清除按钮),不断记得调用onChange可能会很繁琐,因此我可能会将其编写为:

const MyCustomComponent = ({onChange}) => {
    const [value, _setValue] = useState('');

    // Always call onChange when we set the new value
    const setValue = (newVal) => {
        onChange(newVal);
        _setValue(newVal);
    }

    return (
        <input value={value} type='text' onChange={e => setValue(e.target.value)}></input>
        <button onClick={() => setValue("")}>Clear</button>
    )
}

但是现在这只是纠缠不清。


19
很好的解释。我之前也用useEffect作为订阅工具。 唯一仍然困惑我的问题是:如果我还想在组件加载时触发第一个onChange,那该怎么办呢?此时,将useEffect[]依赖项配合使用是否有意义?但是这样做会再次引起lint警告。 - zanona
@zanona,useCallback技巧可行吗?基本上,您不想调用onChange,而是更具体地想要调用在挂载期间传递的onChange函数。因此,您需要一种记住这个函数并在之后调用它的方法。 我还尝试将[!!onChange]添加为依赖项,但似乎不起作用。 - Eric Burel
似乎useCallback也会失败,但是useRef更合适:const initialOnChangeRef = useRef(() => onChange(value)); useEffect(() => { initialOnChangeRef.current(); }, [initialOnChangeRef]); - Eric Burel
这比 useEffect 更有意义,但函数的命名应该改进。例如,只需将函数称为 handleChange,而不要在 _setValue 中使用下划线。 - Micros
1
订阅与副作用部分使理解变得更容易。如果不是在某些更改上触发,有谁知道useEffect的副作用的好例子是什么? - Ivditi Gabeskiria
显示剩余2条评论

86
exhaustive-deps警告的主要目的是防止开发人员在其effect中漏掉依赖项并丢失某些行为。
Facebook核心开发人员Dan Abramov强烈建议保持该规则启用
对于将函数作为依赖项传递的情况,在React FAQ中有一个专门的章节。

https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

简述

如果您必须将一个函数放在依赖数组中:

  • 将函数放在组件之外,这样可以确保引用不会在每次渲染时更改。
  • 如果可以的话,在 effect 之外调用函数,并只使用结果作为依赖项。
  • 如果函数必须在组件范围内声明,则必须使用 useCallback 钩子来记忆化函数引用。仅当回调函数的依赖项发生更改时,引用才会更改。

1
例如 const initialOnChange = useCallback(onChange, []); useEffect(() => initialOnChange(value), [initialOnChange]) 应该可以在挂载时触发函数吗?编辑:不,它不能 :/ 你需要将 onChange 函数作为 useCallback 的依赖项传递,这样才能按预期工作。 - Eric Burel
3
我们为什么不能只将该函数作为依赖项添加,而不必承担useCallback的所有开销? - Isaac Pak
@IsaacPak 如果你在其他地方定义了函数,那么可以这样做!- 如果你在组件上定义了函数,那么它就不是“相同”的函数渲染到渲染(在三重等式意义上),因此会告诉 useEffect 更新并导致无限循环。 - Julix
4
完全同意@IsaacPak的观点。在这方面,React过于复杂。 - Brady Dowling
如果可以的话,请在您的效果内部调用函数,然后将结果仅用作依赖项。原始帖子说相反的话,链接的文档和丹的博客则表示相反的意见(请参见上文)。 - kevin
显示剩余5条评论

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