React hook 之 useEffect

useEffect

useEffect 是一个 React Hook,它允许你将组件与外部系统同步。

语法

useEffect(setup, dependencies?)

参数

  • setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。

  • dependencies:可选参数;setup 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3] 这样内联编写。
    React 将使用 Object.is 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。

传递依赖数组、空数组和不传递依赖项表现会有所区别。

传递依赖数组

如果指定了依赖项,则 Effect 在 初始渲染后以及依赖项变更的重新渲染后 运行。
多个依赖项的时候,任意一个依赖项改变都会导致重新运行。

useEffect(() => { console.log(`${name} ${age} years old.`); }, [name, age]);

空数组

如果你的 Effect 确实没有使用任何响应式值,则它仅在 初始渲染后 运行。

useEffect(() => { // ... }, []); // 不会再次运行(开发环境下除外)

不传递依赖项

如果完全不传递依赖数组,则 Effect 会在组件的 每次单独渲染(和重新渲染)之后 运行。

useEffect(() => { // ... }); // 总是再次运行

常见问题

Effect 在组件挂载时运行了两次

在开发环境下,如果开启严格模式,React 会在实际运行 setup 之前额外运行一次 setup 和 cleanup。

这是一个压力测试,用于验证 Effect 的逻辑是否正确实现。如果出现可见问题,则 cleanup 函数缺少某些逻辑。cleanup 函数应该停止或撤消 setup 函数所做的任何操作。一般来说,用户不应该能够区分 setup 被调用一次(如在生产环境中)和调用 setup → cleanup → setup 序列(如在开发环境中)。

Effect 在每次重新渲染后都运行

首先,请检查是否忘记指定依赖项数组:

useEffect(() => { // ... }); // 🚩 没有依赖项数组:每次重新渲染后重新运行!

如果你已经指定了依赖项数组,你的 Effect 仍循环地重新运行,那是因为你的某个依赖项在每次重新渲染时都是不同的。

你可以通过手动打印依赖项到控制台来调试此问题:

useEffect(() => { // .. }, [serverUrl, roomId]); console.log([serverUrl, roomId]);

当你发现某个依赖项在每次重新渲染都不同时,通常可以通过以下方式之一来解决:

  • 在 Effect 中根据先前 state 更新 state
  • 删除不必要的对象依赖项
  • 删除不必要的函数依赖项
  • 从 Effect 读取最新的 props 和 state

作为最后的手段(如果这些方法没有帮助),使用 useMemo 或 useCallback(用于函数)包装其创建。

Effect 函数一直在无限循环中运行

如果你的 Effect 函数一直在无限循环中运行,那么必须满足以下两个条件:

  • 你的 Effect 函数更新了一些状态。
  • 这些状态的改变导致了重新渲染,从而导致 Effect 函数依赖的状态发生改变。

在开始修复问题之前,问问自己,你的 Effect 是否连接到了某个外部系统(如 DOM、网络、第三方小部件等)。为什么你的 Effect 函数需要设置状态?它是否与外部系统同步?或者你正在试图用它来管理应用程序的数据流?

如果没有外部系统,请考虑 完全删除 Effect 函数 是否可以简化你的逻辑。

如果你真的正在与某个外部系统同步,请考虑为什么以及在何种条件下你的 Effect 函数应该更新状态。是否有任何变化会影响组件的可视输出?如果你需要跟踪一些不用于渲染的数据,使用一个 ref(它不会触发重新渲染)可能更合适。验证你的 Effect 函数不会超过需要地更新状态(并触发重新渲染)。

最后,如果你的 Effect 函数在正确的时机更新了状态,但仍然存在一个循环,那是因为该状态更新导致 Effect 的一个依赖项发生了更改

即使组件没有卸载,cleanup 逻辑也会运行

cleanup 函数不仅在卸载期间运行,也在每个依赖项变更的重新渲染前运行。此外,在开发环境中,React 在组件挂载后会立即额外运行一次 setup + cleanup。

如果你的 cleanup 代码没有相应的 setup 代码,这通常是一种代码异味(code smell):

useEffect(() => { // 🔴 避免:cleanup 逻辑没有相应的 setup 逻辑 return () => { doSomething(); }; }, []);

你的 cleanup 逻辑应该与 setup 逻辑“对称”,并且应该停止或撤销任何 setup 做的事情:

useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]);

我的 Effect 做了一些视觉相关的事情,在它运行之前我看到了一个闪烁

如果 Effect 一定要阻止浏览器绘制屏幕,使用 useLayoutEffect 替换 useEffect。请注意,绝大多数的 Effect 都不需要这样。只有当在浏览器绘制之前运行 Effect 非常重要的时候才需要如此:例如,在用户看到 tooltip 之前测量并定位它。