发布于 2026-01-06 8 阅读
0

自定义 React Hook 用于取消网络调用,然后将 API 调用与组件生命周期同步。

自定义 React Hook 用于取消网络调用,然后将 API 调用与组件生命周期同步。

首先,我们来谈谈我们试图解决的问题。

如果你使用 React,几乎不可能从未在浏览器控制台中看到过这个错误日志。



Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
    in TextLayerInternal (created by Context.Consumer)
    in TextLayer (created by PageInternal) index.js:1446
d/console[e]


Enter fullscreen mode Exit fullscreen mode

说实话,在你深入了解组件生命周期之后,这可能是最难理解的事情之一。这个错误基本上意味着你使用了包含状态变更(我指的是 `setState`)的异步代码块,从而导致了内存泄漏。

虽然在大多数情况下是无害的,但仍然存在堆内存使用未优化、代码崩溃以及其他各种问题的可能性。

现在我们来谈谈解决方案。

嗯,解决这个问题的方法有很多,其中最流行的一种是使用逻辑来检查组件是否仍然挂载在组件树中,只有当组件挂载成功时才进行状态更改操作。你可能会觉得这样就能解决你的问题了,对吧?对吧?嗯
……某种程度上是这样。我的意思是,让我们退一步,想想一个非常著名的 hook:useIsMounted。

现在想象一下这样的场景:你在组件挂载时发起 API 调用,使用这个钩子,你只会在组件仍然挂载时才改变它的状态。



  const isMounted = useIsMounted();
  const [value, setValue] = useState();

  useEffect(() => {
    fetch('some resource url')
      .then((data) => {
        return data.json();
      })
      .then((data) => {
        if (isMounted()) {
          setValue(data);
        }
      });
  }, [input]);


Enter fullscreen mode Exit fullscreen mode

这段代码看起来完全没问题,肯定不会报错吧?嗯,我的意思是,它确实能运行!

  • 你还在发起 fetch 请求吗?

  • 你难道没有履行承诺吗?如果组件已经卸载了,你显然不需要再做这件事了,对吧?

而且,根据你的应用程序对 API 的依赖程度,避免满足所有网络请求可能会给你带来意想不到的好处。

那么我们该怎么做呢?我们可以直接取消正在进行的请求。事实上,现代浏览器早就具备这个功能了。

AbortController接口允许你中止任何 Web 请求。

目前浏览器的fetch API 和Axios都正式支持 AbortControllers。

现在我们可以就此结束了,但为了让它看起来更酷炫一些,我们来创建一个自定义钩子,并看一个实际示例。

使用 useAbortedEffect 钩子在组件卸载时取消所有网络请求。



import { useEffect } from 'react';

const useAbortedEffect = (
  effect: (signal: AbortSignal) => Function | void,
  dependencies: Array<any>
) => {
  useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;
    const cleanupEffect = effect(signal);

    return () => {
      if (cleanupEffect) {
        cleanupEffect();
      }
      abortController.abort();
    };
  }, [...dependencies]);
};

export default useAbortedEffect;



Enter fullscreen mode Exit fullscreen mode

现在让我们来详细分析一下。我们的自定义 effect 接收一个回调函数,该函数接受一个 AbortSignal 参数和一个依赖项数组作为参数,就像其他 effect hook 一样。在 useEffect 中,我们实例化一个 AbortController 并将信号传递给 effect 回调函数,这样我们想要发起的任何网络请求都能接收到这个信号。这有助于我们控制 effect 回调函数中声明的所有 API 的执行周期。在 useEffect 的 unmount 回调函数中,我们中止控制器,所有在 effect 中进行的网络调用都会被浏览器取消。

让我们举个例子来体会一下这个钩子的含义。

在这个例子中,我们将使用 React Router 的 Outlet API 创建 3 个嵌套路由,使每个页面依次挂载和重新挂载,以便我们可以监控网络选项卡。



import { Outlet, useNavigate } from 'react-router-dom';

const Home = () => {
  const navigate = useNavigate();
  return (
    <div>
      Home Page
      <div className="column">
        <button onClick={() => navigate('/first')}>First</button>
        <button onClick={() => navigate('/second')}>Second</button>
        <button onClick={() => navigate('/third')}>Third</button>
        <Outlet />
      </div>
    </div>
  );
};

export default Home;



Enter fullscreen mode Exit fullscreen mode

在我们的每个页面的第一、第二和第三页中,我们将使用自定义钩子来触发 API,并将信号参数传递给 fetch 和 Axios 的信号属性,以便控制请求(请记住,此步骤是必需的,因为任何没有此信号的请求都不会被取消)。

首页组件看起来大概是这样的



  //example with axios
  useAbortedEffect(
    (signal) => {
      axios
        .get('https://jsonplaceholder.typicode.com/posts', {
          signal
        })
        .then((data) => {
          console.log('First API call');
        })
        .catch((e: any) => {
          if (e.name === 'CanceledError') {
            console.log('First API aborted');
          }
        });
    },
    []
  );

return (
    <div>
      First Page
      <div
        style={{
          display: 'flex',
          gap: '10px',
          marginTop: '20px'
        }}>
        <button onClick={() => setCount(count + 1)}>Click </button>
        <span>Count : {count}</span>
      </div>
    </div>
  );


Enter fullscreen mode Exit fullscreen mode

由于我使用的是 JSON 占位符作为端点,因此要注意到网络调用的任何待处理状态都会很棘手,所以我们来模拟一个较慢的网络。
在开发者工具中,打开网络选项卡,然后从网络下拉菜单中选择“慢速 3G”
(我使用的是 Chrome 浏览器)。

Chrome 网络标签页 slow3G

启动应用程序后,请按顺序点击第一个、第二个和第三个链接,然后查看“网络”选项卡。
网络选项卡屏幕截图

既然我们在自定义效果的每一步都使用了 console.log,那我们也来看看控制台输出吧。
控制台屏幕截图

正如你所见,在连续挂载和重新挂载第一页和第二页之后,所有待处理的请求都因为中止信号而被取消,我们也可以看到相应的控制台日志。这与 JavaScript 中的防抖机制类似,但不同之处在于,我们不是使用事件循环中的定时器进行防抖,而是在浏览器本身中进行网络请求防抖。

使用这个钩子可以达到什么效果?

这取决于你的应用程序架构以及它对 API 的依赖程度,理论上你可以……

  • 避免组件中出现内存泄漏

  • 针对您的组件创建原子 API 事务

  • 减少 API 调用次数。

示例的 GitHub 仓库

请对文章提出意见,以便我改进文章并改正错误,提前感谢。

欢迎在其他平台关注我。

文章来源:https://dev.to/mr_mornin_star/how-to-synchronize-your-api-calls-with-your-component-lifecycle-in-react-with-a-custom-hook-82d