Skip to content

使用useEffect的正确姿势

为什么useEffect必不可少,以及为什么在数据获取时应避免使用它。

最近我开始学习前端开发的基础知识。我观察到,许多前端开发者通常会使用useEffect来获取数据。然而,在大多数情况下,这并不是一种理想的做法。在本文中,我将解释为什么不推荐这样做,以及我们如何替代或者改进这种方法。但首先,我们需要理解什么是副作用以及useEffect是如何工作的。

理解React中的副作用

在 React 中,副作用是指发生在组件外部的后果性行为。理想情况下,React 组件应该是完全纯粹的,这意味着它们不应修改调用之前存在的任何对象或变量,并且对相同输入应始终产生相同输出。例如:

jsx
function Square({ number }) {
  return (
    <div>
      The square of {number} is {number * number}.
    </div>
  );
}
function Square({ number }) {
  return (
    <div>
      The square of {number} is {number * number}.
    </div>
  );
}

上面的例子看起来很简单,但它遵守了上述的基本规则,保证了代码的可预测性。

然而,有时我们必须在代码中引入副作用,例如触发动画或从外部 API 获取数据。在这种情况下,我们可以使用 React 的事件处理函数(例如点击按钮时触发的函数)。但如果我们需要在组件挂载后立即触发副作用,可以使用 useEffect 钩子。

useEffect 的工作原理

useEffect 钩子会在组件初次渲染后立即在客户端运行。它接受两个参数:一个包含副作用逻辑的函数,以及一个依赖项数组。当组件重新渲染时,如果依赖项数组中的某些值发生了变化,useEffect 中的函数会再次触发。

jsx
useEffect(() => {
  // 副作用函数
  console.log(randomValue);
}, [randomValue]); // 依赖项数组
useEffect(() => {
  // 副作用函数
  console.log(randomValue);
}, [randomValue]); // 依赖项数组

在useEffect中,还可以返回一个清理函数,该函数会在组建卸载前调用。清理函数用于还原副作用的更改,例如关闭连接,移除事件监听器或者清除计时器。它还可以帮助避免在获取数据时发生竞态条件(race condition)。

jsx
useEffect(() => {
  const options = { roomId };
  const connection = createConnection();
  connection.connect();

  return () => connection.disconnect(); // 清理函数
}, [roomId]);
useEffect(() => {
  const options = { roomId };
  const connection = createConnection();
  connection.connect();

  return () => connection.disconnect(); // 清理函数
}, [roomId]);

为什么需要useEffect

React 组件通过 useEffect 与外部系统进行同步非常重要,因为 React 有其特定的生命周期阶段:挂载、渲染和卸载。而外部 API 通常不会与这些生命周期阶段完全匹配。因此,useEffect 对于这种同步至关重要,因为 React 本身无法直接感知外部操作,例如数据获取或分析。

一个典型的例子是,在用户按下 Escape 键时关闭模态框。在这种场景中,useEffect 可帮助我们实现与用户行为的同步:

jsx
const Modal = ({ closeModal }) => {
  React.useEffect(() => {
    function handleKeyDown(event) {
      if (event.code === "Escape") {
        closeModal();
      }
    }
    window.addEventListener("keydown", handleKeyDown);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

  return <div>.....</div>;
};
const Modal = ({ closeModal }) => {
  React.useEffect(() => {
    function handleKeyDown(event) {
      if (event.code === "Escape") {
        closeModal();
      }
    }
    window.addEventListener("keydown", handleKeyDown);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

  return <div>.....</div>;
};

通过 useEffect,我们可以确保事件监听器正确管理,并与 Modal 组件的生命周期保持同步,从而高效地处理用户按键事件。

为什么我们不应该用 useEffect 获取数据

听起来有点矛盾,对吧?如果 useEffect 是用来同步外部数据的,为什么不推荐用它来获取数据呢?答案很简单,因为我们有更好的选择。

在实际项目中,仅仅通过 useEffect 获取数据往往不够。这样做可能导致以下问题:

竞态条件(Race Conditions)

不必要的带宽消耗

错误可能导致应用崩溃

为了改进,我们可以引入缓存来减少带宽消耗,处理错误、实现失败重试,以及通过去重避免竞态条件。但这些改进会带来扩展性、维护性和调试上的挑战。

我们有哪些替代方案?

如果你使用的是基于 React 的框架(例如 Next.js),它本身就有内置的数据获取机制。在较新的版本中,甚至可以直接在服务端获取数据。

同时,还有一些优秀的第三方库,比如 SWRReact Query。这些库可以简化数据获取的流程,并减少需要编写的代码量。

如果没有使用框架,而是直接使用 React 来获取数据,官方建议通过忽略旧请求来处理竞态条件。此外,还可以将 useEffect 的逻辑提取到自定义 Hook 中,以提高代码的可维护性。

以下是一个例子:

jsx
useEffect(() => {
  let ignore = false;

  fetch(url)
    .then((response) => response.json())
    .then((json) => {
      if (!ignore) {
        setData(json);
      }
    });

  return () => {
    ignore = true;
  };
}, [url]);
useEffect(() => {
  let ignore = false;

  fetch(url)
    .then((response) => response.json())
    .then((json) => {
      if (!ignore) {
        setData(json);
      }
    });

  return () => {
    ignore = true;
  };
}, [url]);

通过这种“忽略”的技巧,可以避免组件在快速切换时因数据响应延迟而显示错误内容。例如,用户点击第1页后又快速切换到第2页时,第1页的数据响应到达后不会覆盖第2页的数据。

结论

由于扩展性问题,仅通过 useEffect 获取数据并不是最佳选择。建议使用外部库或基于 React 的框架(如 Next.js)。

希望本文能帮助你更好地理解 useEffect,并引导你朝着更加可维护、易调试的代码方向努力。

Updated Date:

Light tomorrow with today.