如何取消Axios Get请求?月度切换请求竞态问题排查
解决Axios CancelToken无法取消重复请求的问题
核心问题分析
- 你的代码中,
CancelToken.source()是在useStatsHook的顶层声明的,每次Hook执行(组件重渲染或调用get方法)都会创建新的source实例,导致旧请求的cancelToken与新source无关联,根本无法取消旧请求。 - 发起新请求前没有主动触发取消操作,旧请求完成后依然会更新状态,造成数据跳转刷新。
修复方案
通过useRef保留CancelToken source的跨渲染引用,在新请求发起前主动取消旧请求,同时添加卸载清理逻辑避免内存泄漏:
import { atom, useAtom } from "jotai"; import axios from "axios"; import queryString from "query-string"; import { useRef, useEffect } from "react"; // Types import { Revenue, RevenueQ, RevenueFunnelTypes, TotalCardsTypes, } from "../../lib/types"; export type Status = "idle" | "loading"; export type Values = { revenueFunnel: RevenueFunnelTypes; totals: TotalCardsTypes; revenue: Revenue | null; revenueQ: RevenueQ | null; }; export const rowsStatusAtom = atom<Status>("idle"); // 补上原代码缺失的valuesAtom声明 const valuesAtom = atom<Values>({ revenueFunnel: {} as RevenueFunnelTypes, totals: {} as TotalCardsTypes, revenue: null, revenueQ: null, }); type ApiRowsResponse = { body: any; }; type State = { values: Values; status: Status; actions: { get: ( type: "revenue" | "revenue-quarters" | "revenue-funnel" | "totals", date?: string ) => Promise<State["values"]>; }; }; // Services const createApiUrl = (route?: string, params?: any) => { return queryString.stringify({ path: route, ...params, }); }; export default function useStats(): State { const [values, setValues] = useAtom(valuesAtom); const [status, setStatus] = useAtom(rowsStatusAtom); // 用useRef存储CancelToken source,跨渲染周期保留引用 const cancelSourceRef = useRef<axios.CancelTokenSource | null>(null); // 组件卸载时取消未完成请求 useEffect(() => { return () => { cancelSourceRef.current?.cancel("组件卸载,取消请求"); }; }, []); const getCurrentValues = async function ( type: "revenue" | "revenue-quarters" | "revenue-funnel" | "totals", date?: string ): Promise<ApiRowsResponse | any> { const url = createApiUrl("statistics", { type: type, date, }); return axios .get(`?${url}`, { cancelToken: cancelSourceRef.current?.token }) .then(response => response.data) .catch(thrown => { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); throw thrown; // 抛出取消错误,阻止后续状态更新 } else { // 处理业务错误 throw thrown; } }) }; const get = async ( type: "revenue" | "revenue-quarters" | "revenue-funnel" | "totals", date?: string ): Promise<State["values"]> => { // 取消之前的未完成请求 cancelSourceRef.current?.cancel("新请求发起,取消旧请求"); // 创建新的CancelToken source cancelSourceRef.current = axios.CancelToken.source(); setStatus("loading"); try { const data = await getCurrentValues(type, date); // 更新对应状态 if (type === "revenue-funnel") { setValues(prev => ({ ...prev, revenueFunnel: data.body })); } else if (type === "revenue") { setValues(prev => ({ ...prev, revenue: data.body })); } else if (type === "revenue-quarters") { setValues(prev => ({ ...prev, revenueQ: data.body })); } else if (type === "totals") { setValues(prev => ({ ...prev, totals: data.body })); } return data.body; } catch (err) { // 忽略取消类错误,只处理业务错误 if (!axios.isCancel(err)) { // 添加错误处理逻辑 } return values; } finally { setStatus("idle"); // 清空source,避免重复取消 cancelSourceRef.current = null; } }; return { values, status, actions: { get }, }; }
关键优化点
useRef存储source:确保跨渲染周期能拿到同一个CancelToken引用,实现旧请求的取消逻辑。- 请求前主动取消:每次调用
get方法时先取消未完成的旧请求,再发起新请求,保证同一时间只有一个请求在进行。 - 错误边界处理:通过
try/catch/finally过滤取消类错误,避免取消请求后更新状态;统一在finally中重置状态。 - 卸载清理:组件卸载时取消未完成请求,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Gurdt
相关产品推荐
相关产品推荐

