发布于 2026-02-19, 更新于 2026-02-20
React Query(现名为 TanStack Query)是一个用于 React 应用的服务端状态管理库。它主要解决了以下问题:
核心理念是 SWR (Stale-While-Revalidate) :先返回缓存中的陈旧数据(Stale),同时在后台发起请求获取最新数据(Revalidate),拿到后更新 UI。
useQuery (读数据)用于获取数据:
queryKey: 唯一标识符,依赖变化时自动触发重新请求。queryFn: 返回 Promise 的异步函数。staleTime: 数据多久变为陈旧(默认 0ms,即立即过期)。enabled: 是否自动运行(依赖项检查)。useMutation (写数据)用于创建/更新/删除数据:
mutationFn: 执行副作用的异步函数。onSuccess: 成功回调,常用于使相关 Query 失效(Invalidation)以触发刷新。useQueryClient (全局操作)用于获取 QueryClient 实例,操作缓存:
invalidateQueries: 标记查询失效,强制重新获取。setQueryData: 手动更新缓存(乐观更新)。removeQueries: 直接移除匹配的缓存数据。区别概览:
invalidateQueries**:把匹配的 query 标记为过期(stale),保留缓存数据;如果该 query 正在被组件使用,会触发重新请求removeQueries**:直接把匹配的 query 从缓存里移除;下次使用时等同于“第一次加载”何时用哪一个:
invalidateQueriesremoveQueries在管理端的直觉用法:
invalidateQueriesremoveQueries项目主要在 管理后台 使用 React Query。
项目在 QueryProvider 中配置了 React Query,并采用了 Next.js App Router 推荐的单例模式。
staleTime: 60 * 1000 (1分钟),减少不必要的后台刷新// QueryProvider.tsx
'use client';
import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode } from 'react';
// 创建 Client 实例的工厂函数
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 默认缓存 1 分钟
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// 服务端预渲染,为每个请求创建一个全新的 QueryClient 实例
return makeQueryClient();
} else {
// 客户端使用单例模式,复用缓存
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function QueryProvider({ children }: { children: ReactNode }) {
const client = getQueryClient();
return (
<QueryClientProvider client={client}>
{children}
</QueryClientProvider>
);
}
以文章列表为例,项目结合 useSearchParams 和 zod Schema 来管理查询参数,并将其作为 queryKey 的一部分。
流程:
zod 校验参数。queryKey,包含所有依赖项。useQuery。// AdminPostsPage.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import { fetchAdminPosts } from '@/app/admin/_api';
export default function AdminPostsPage() {
const searchParams = useSearchParams();
// 1. 解析并校验 URL 参数
const query = useMemo(() => {
return {
page: Number(searchParams.get('page') || 1),
count: Number(searchParams.get('count') || 20),
status: searchParams.get('status') ? Number(searchParams.get('status')) : undefined,
// ...其他参数
};
}, [searchParams]);
// 2. 发起查询
const posts = useQuery({
// Query Key: 包含所有筛选条件,任意变动都会触发重新请求
queryKey: ['admin', 'posts', query.page, query.count, query.status],
// Query Function: 调用 API
queryFn: () => fetchAdminPosts(query),
});
if (posts.isLoading) return <div>加载中...</div>;
return (
<div>
{/* 渲染列表 */}
{posts.data?.data?.items.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
以保存文章为例,使用 useMutation 提交数据,并在成功后刷新相关缓存。
流程:
useQueryClient 获取实例。mutationFn 执行 API 调用。onSuccess 中调用 invalidateQueries,使 'admin', 'posts' 相关的查询失效,从而触发列表和详情页的自动刷新。// PostEditor.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateAdminPost, createAdminPost } from '@/app/admin/_api';
export default function PostEditor({ initial }) {
const queryClient = useQueryClient();
// 定义 Mutation
const saveMutation = useMutation({
mutationFn: (payload) =>
payload.id ? updateAdminPost(payload.id, payload.data) : createAdminPost(payload.data),
onSuccess: (data, variables) => {
// 成功后刷新缓存
// 1. 刷新文章列表
queryClient.invalidateQueries({ queryKey: ['admin', 'posts'] });
// 2. 刷新特定文章详情(如果是更新)
if (variables.id) {
queryClient.invalidateQueries({ queryKey: ['admin', 'posts', variables.id] });
}
// 3. 刷新仪表盘统计
queryClient.invalidateQueries({ queryKey: ['admin', 'dashboard'] });
},
});
const onSubmit = async (data) => {
try {
await saveMutation.mutateAsync({ id: initial.id, data });
alert('保存成功');
} catch (error) {
alert('保存失败');
}
};
return (
<button onClick={() => onSubmit({ title: '新标题' })} disabled={saveMutation.isPending}>
{saveMutation.isPending ? '保存中...' : '保存'}
</button>
);
}
我们在 src/app/admin/_api/index.ts 中封装了所有 API 请求,统一处理 Cookie 凭证(credentials: 'include')和错误抛出。
// api/index.ts
async function jsonRequest(url: string, init: RequestInit) {
const res = await fetch(url, {
...init,
credentials: 'include', // 携带 Cookie (Session)
headers: { 'content-type': 'application/json' },
});
const json = await res.json().catch(() => null);
if (!res.ok) throw new Error(json?.error?.message || '请求失败');
return json;
}
export async function fetchAdminPosts(params) {
// ...构建 URLSearchParams
return jsonRequest(`/api/admin/posts?${params}`, { method: 'GET' });
}
['admin', 'posts', id] 这样的数组结构非常清晰,方便精确控制缓存失效。'use client') 中使用 Hooks,但在 Provider 层要做好 SSR 的兼容处理。useQuery 的依赖机制。