请求取消与 AbortController

TanStack Query 的 queryFn 接收 AbortSignal 参数,可在组件卸载或 queryKey 变化时自动取消在途请求,防止竞态和资源浪费。

#type / howto #status / evergreen #tech / dev / frontend #resource / react

[!info] related notes

请求取消与 AbortController

核心问题

用户快速切换列表项(A → B → C),三个请求同时在途:

请求 A 发出 → 请求 B 发出 → 请求 C 发出
请求 B 先返回 → 写入缓存
请求 A 后返回 → 覆盖缓存(错误!应该是 C 的数据)
请求 C 最后返回 → 写入缓存(正确,但之前的 A 已经污染了)

竞态条件:旧请求后返回,覆盖了新数据。

TanStack Query 的自动取消

当 queryKey 变化或组件卸载时,TanStack Query 会自动调用 AbortController.abort()。你只需要在 queryFn 里使用 signal

useQuery({
  queryKey: ['user', id],
  queryFn: async ({ signal }) => {
    const response = await fetch(`/api/users/${id}`, { signal });
    return response.json();
  },
});

fetch 原生支持 signal,当 abort 时会抛出 AbortError

使用场景

fetch 原生支持

// ✅ 最简单的方式
useQuery({
  queryKey: ['data', id],
  queryFn: async ({ signal }) => {
    const res = await fetch(`/api/data/${id}`, { signal });
    if (!res.ok) throw new Error('请求失败');
    return res.json();
  },
});

Axios

import axios from 'axios';

useQuery({
  queryKey: ['data', id],
  queryFn: async ({ signal }) => {
    const { data } = await axios.get(`/api/data/${id}`, { signal });
    return data;
  },
});

自定义请求库

useQuery({
  queryKey: ['data', id],
  queryFn: async ({ signal }) => {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', `/api/data/${id}`);

      // 监听 abort 信号
      signal.addEventListener('abort', () => {
        xhr.abort();
        reject(new DOMException('Aborted', 'AbortError'));
      });

      xhr.onload = () => resolve(JSON.parse(xhr.responseText));
      xhr.onerror = () => reject(new Error('请求失败'));
      xhr.send();
    });
  },
});

长轮询场景

useQuery({
  queryKey: ['status', jobId],
  queryFn: async ({ signal }) => {
    while (!signal.aborted) {
      const res = await fetch(`/api/jobs/${jobId}/status`, { signal });
      const data = await res.json();

      if (data.status === 'completed' || data.status === 'failed') {
        return data;
      }

      // 等 2 秒再轮询
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
    throw new DOMException('Aborted', 'AbortError');
  },
  refetchInterval: false,  // 禁用自动 refetch,用自定义轮询
});

手动取消

cancelQueries

const queryClient = useQueryClient();

// 取消特定 query
await queryClient.cancelQueries({ queryKey: ['user', id] });

// 取消所有以 ['user'] 开头的 query
await queryClient.cancelQueries({ queryKey: ['user'] });

// 取消所有 query
await queryClient.cancelQueries();

典型用法:乐观更新前

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (newUser) => {
    // 1. 取消在途请求,防止覆盖乐观数据
    await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });

    // 2. 保存旧数据用于回滚
    const previous = queryClient.getQueryData(['user', newUser.id]);

    // 3. 写入乐观数据
    queryClient.setQueryData(['user', newUser.id], newUser);

    return { previous };
  },
  onError: (err, newUser, context) => {
    // 4. 回滚
    queryClient.setQueryData(['user', newUser.id], context.previous);
  },
  onSettled: () => {
    // 5. 最终同步
    queryClient.invalidateQueries({ queryKey: ['user'] });
  },
});

signal 的其他用途

监听取消事件

useQuery({
  queryKey: ['data'],
  queryFn: async ({ signal }) => {
    // 注册清理逻辑
    const cleanup = () => console.log('请求被取消');

    if (signal.aborted) {
      cleanup();
      throw new DOMException('Aborted', 'AbortError');
    }

    signal.addEventListener('abort', cleanup, { once: true });

    try {
      const res = await fetch('/api/data', { signal });
      return res.json();
    } finally {
      signal.removeEventListener('abort', cleanup);
    }
  },
});

传递给子函数

async function fetchWithRetry(url: string, signal?: AbortSignal, retries = 3) {
  for (let i = 0; i < retries; i++) {
    if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');

    try {
      const res = await fetch(url, { signal });
      return res.json();
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') throw err;
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

useQuery({
  queryKey: ['data'],
  queryFn: ({ signal }) => fetchWithRetry('/api/data', signal),
});

常见错误

不处理 AbortError

// ❌ AbortError 会被当作真正的错误
useQuery({
  queryFn: async ({ signal }) => {
    try {
      const res = await fetch('/api/data', { signal });
      return res.json();
    } catch (err) {
      console.error('请求失败', err);  // AbortError 也会走到这
      throw err;
    }
  },
});

// ✅ 区分取消和真正的错误
useQuery({
  queryFn: async ({ signal }) => {
    try {
      const res = await fetch('/api/data', { signal });
      return res.json();
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        throw err;  // 让 TanStack Query 处理
      }
      console.error('请求失败', err);
      throw err;
    }
  },
});

忘记传递 signal

// ❌ 没用 signal,请求无法自动取消
useQuery({
  queryFn: async () => {
    const res = await fetch('/api/data');
    return res.json();
  },
});

// ✅ 传递 signal
useQuery({
  queryFn: async ({ signal }) => {
    const res = await fetch('/api/data', { signal });
    return res.json();
  },
});
创建于 2026/7/3 更新于 2026/7/15