请求取消与 AbortController
TanStack Query 的 queryFn 接收 AbortSignal 参数,可在组件卸载或 queryKey 变化时自动取消在途请求,防止竞态和资源浪费。
#type / howto
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: 缓存失效模式
- 关联: 取消传播
请求取消与 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();
},
});