TanStack Query 请求取消与请求重试指南

在开始之前,我们需要先安装必要的依赖包。TanStack Query 提供了统一的 API 设计,React 版本和 Vue 版本的安装方式略有不同,但核心概念完全一致。

对于 React 项目,我们需要安装 @tanstack/react-query 包,同时为了方便演示,还会安装 axios 作为 HTTP 客户端。安装命令如下:

pnpm install @tanstack/react-query axios

对于 Vue3 项目,则需要安装 @tanstack/vue-query 包:

pnpm install @tanstack/vue-query axios

安装完成后,我们需要在应用入口处配置 QueryClientProvider。React 版本通常在 main.tsx 或 App.tsx 中进行配置,而 Vue3 版本则在 main.ts 中进行配置。这一步是必不可少的,因为 TanStack Query 需要通过 Provider 来共享查询客户端实例。

请求取消的实现

TanStack Query 提供了两种取消请求的方式:手动取消和自动取消。手动取消通过 AbortController 来实现,我们可以精确控制何时取消请求;自动取消则由库自动处理,当查询键(queryKey)发生变化或者组件卸载时,未完成的请求会被自动取消。

1、基于 AbortController 手动取消请求

TanStack Query 会在适当的时候(如组件卸载、queryClient.cancelQueries)自动生成一个 AbortSignal,并注入到你的 queryFn 中。你只需要把这个 signal 传给 axios 请求即可。

下面是手动取消请求的完整示例: 在这个示例中,我们创建了一个搜索功能。当用户输入时,我们会先取消之前正在进行的请求,然后创建新的 AbortController 并发起新的请求。这样可以确保用户看到的是最新的搜索结果,而不是被旧请求的结果覆盖。

import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useState, useRef } from 'react';

function SearchComponent() {
  const [keyword, setKeyword] = useState('');
  const abortControllerRef = useRef<AbortController | null>(null);
  const queryClient = useQueryClient();

  const { data, isLoading, isFetching } = useQuery({
    queryKey: ['search', keyword],
    queryFn: async () => {
      // 取消之前的请求
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
      
      // 创建新的 AbortController
      abortControllerRef.current = new AbortController();
      
      const response = await axios.get('/api/search', {
        params: { q: keyword },
        signal: abortControllerRef.current.signal,
      });
      
      return response.data;
    },
    enabled: keyword.length > 0,
  });

  // 手动取消按钮
  const handleCancel = () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
  };

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <button onClick={handleCancel}>取消请求</button>
    </div>
  );
}
function fetchTodo({ signal }) {
  return axios.get('/api/todo', { signal }).then(res => res.data);
}

useQuery({
  queryKey: ['todo'],
  queryFn: fetchTodo,
});

2、基于 queryClient.cancelQueries 自动取消请求

当查询键(queryKey)发生变化或者组件卸载时,TanStack Query 会自动取消未完成的请求。这是一种非常方便的机制,无需手动编写取消逻辑。

import { useQueryClient } from '@tanstack/react-query';

function TodoList() {
  const queryClient = useQueryClient();

  const handleCancel = () => {
    // 取消所有 queryKey 以 ['todos'] 开头的正在进行的请求
    queryClient.cancelQueries({ queryKey: ['todos'] });
  };

  return <button onClick={handleCancel}>取消加载</button>;
}
  • 该查询的 Promise 会被 reject,抛出一个 CancelledError。
  • isLoading 变为 false,error 变为取消错误(可通过 query.isCanceled 判断)。
  • 不会自动删除已有数据,也不会触发重新获取。
const query = useQuery(...);

if (query.isCanceled) {
  console.log('请求被主动取消');
}

在全局错误处理中:

if (queryClient.isCancelledError(error)) {
  // 是被取消的错误
}

请求重试

默认情况下,TanStack Query 会在请求失败时自动重试 3 次,每次重试之间的间隔会逐渐增加(指数退避)。这种设计是经过深思熟虑的:初始重试间隔较短,可以快速恢复失败的请求;如果连续失败,说明网络可能存在较大问题,此时增加间隔可以避免对服务器造成额外压力。

下面是请求重试的完整示例,展示了不同的重试配置方式:

import { useQuery } from '@tanstack/react-query';
import axios from 'axios;

// 模拟会失败的请求
const fetchUnreliableData = async (): Promise<{ message: string }> => {
  const shouldFail = Math.random() > 0.7; // 70%概率失败
  if (shouldFail) {
    throw new Error('网络请求失败');
  }
  const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
  return response.data;
};

export function RetryExample() {
  // 方式一:使用默认重试配置(3次)
  const { data: data1, error: error1, refetch: refetch1 } = useQuery({
    queryKey: ['unreliable-default'],
    queryFn: fetchUnreliableData,
  });

  // 方式二:自定义重试次数
  const { data: data2, error: error2, refetch: refetch2 } = useQuery({
    queryKey: ['unreliable-custom-count'],
    queryFn: fetchUnreliableData,
    retry: 5, // 重试5次
  });

  // 方式三:使用重试函数(自定义重试逻辑)
  const { data: data3, error: error3, refetch: refetch3 } = useQuery({
    queryKey: ['unreliable-function'],
    queryFn: fetchUnreliableData,
    retry: (failureCount, error) => {
      // 只在网络错误时重试,404等客户端错误不重试
      if (axios.isAxiosError(error)) {
        return !error.response && failureCount < 3;
      }
      return failureCount < 3;
    },
  });

  // 方式四:设置重试延迟
  const { data: data4, error: error4, refetch: refetch4 } = useQuery({
    queryKey: ['unreliable-delay'],
    queryFn: fetchUnreliableData,
    retry: 3,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    // 重试延迟:1秒、2秒、4秒,最大30秒
  });

  return (
    <div className="p-4 space-y-6">
      <div>
        <h3>默认重试(3次)</h3>
        <button onClick={() => refetch1()}>请求数据</button>
        {error1 && <p className="text-red-500">错误: {(error1 as Error).message}</p>}
        {data1 && <p>成功: {data1.title}</p>}
      </div>

      <div>
        <h3>自定义重试次数(5次)</h3>
        <button onClick={() => refetch2()}>请求数据</button>
        {error2 && <p className="text-red-500">错误: {(error2 as Error).message}</p>}
        {data2 && <p>成功: {data2.title}</p>}
      </div>

      <div>
        <h3>自定义重试逻辑</h3>
        <button onClick={() => refetch3()}>请求数据</button>
        {error3 && <p className="text-red-500">错误: {(error3 as Error).message}</p>}
        {data3 && <p>成功: {data3.title}</p>}
      </div>

      <div>
        <h3>指数退避重试延迟</h3>
        <button onClick={() => refetch4()}>请求数据</button>
        {error4 && <p className="text-red-500">错误: {(error4 as Error).message}</p>}
        {data4 && <p>成功: {data4.title}</p>}
      </div>
    </div>
  );
}