自定义 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]
说实话,在你深入了解组件生命周期之后,这可能是最难理解的事情之一。这个错误基本上意味着你使用了包含状态变更(我指的是 `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]);
这段代码看起来完全没问题,肯定不会报错吧?嗯,我的意思是,它确实能运行!
但
-
你还在发起 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;
现在让我们来详细分析一下。我们的自定义 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;
在我们的每个页面的第一、第二和第三页中,我们将使用自定义钩子来触发 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>
);
由于我使用的是 JSON 占位符作为端点,因此要注意到网络调用的任何待处理状态都会很棘手,所以我们来模拟一个较慢的网络。
在开发者工具中,打开网络选项卡,然后从网络下拉菜单中选择“慢速 3G”
(我使用的是 Chrome 浏览器)。
启动应用程序后,请按顺序点击第一个、第二个和第三个链接,然后查看“网络”选项卡。
既然我们在自定义效果的每一步都使用了 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
