You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何取消Axios Get请求?月度切换请求竞态问题排查

解决Axios CancelToken无法取消重复请求的问题

核心问题分析

  • 你的代码中,CancelToken.source()是在useStats Hook的顶层声明的,每次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 },
  };
}

关键优化点

  1. useRef存储source:确保跨渲染周期能拿到同一个CancelToken引用,实现旧请求的取消逻辑。
  2. 请求前主动取消:每次调用get方法时先取消未完成的旧请求,再发起新请求,保证同一时间只有一个请求在进行。
  3. 错误边界处理:通过try/catch/finally过滤取消类错误,避免取消请求后更新状态;统一在finally中重置状态。
  4. 卸载清理:组件卸载时取消未完成请求,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Gurdt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 10:09:49