# 使用useEffect的正确姿势

> Site index: <https://blog.suxiong.me/llms.txt>

Original: [使用useEffect的正确姿势](https://blog.suxiong.me/blog/the-right-way-to-use-useeffect)

Author: Suxiong

Date: 2024-10-09

深入探讨React中useEffect的使用方法和最佳实践。文章解释了为什么useEffect必不可少，以及在数据获取时为什么应该避免使用它。同时介绍了useEffect的工作原理、使用场景和替代方案，帮助开发者更好地理解和使用这个重要的React Hook。

Tags: React / Hooks / useEffect / 前端开发

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

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

## 理解React中的副作用

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

```jsx
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中，还可以返回一个清理函数，该函数会在组建卸载前调用。清理函数用于还原副作用的更改，例如关闭连接，移除事件监听器或者清除计时器。它还可以帮助避免在获取数据时发生竞态条件（race condition）。

```jsx
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>;
};
```

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

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

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

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

• **竞态条件（Race Conditions）**

• **不必要的带宽消耗**

• **错误可能导致应用崩溃**

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

## 我们有哪些替代方案？

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

同时，还有一些优秀的第三方库，比如 **SWR** 和 **React 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]);
```

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

## 结论

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

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