TanStack Query 快速入门指南
了解如何在项目中使用 TanStack Query 进行状态管理和 API 数据获取,以及与 Hono.js API 的集成实践
TanStack Query 快速入门指南
TanStack Query 是一个强大的数据获取、缓存、同步和更新库,专为 React 应用程序设计。它帮助我们更好地管理服务端状态,减少样板代码,提供出色的用户体验。
为什么使用 TanStack Query?
- 自动缓存: 智能缓存和重复请求去重
- 后台更新: 自动在后台重新获取过期数据
- 乐观更新: 提供乐观更新功能,让用户界面响应更快
- 错误重试: 内置错误重试机制
- 分页和无限查询: 原生支持分页和无限滚动
- 窗口焦点重获取: 当窗口重新获得焦点时自动刷新数据
项目配置
1. Query Client 设置
我们的项目已经配置了 Query Client,位于 src/modules/shared/lib/query-client.ts
:
export function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// 默认缓存时间设置
staleTime: 2 * 60 * 1000, // 2分钟 - 数据被认为是新鲜的时间
gcTime: 10 * 60 * 1000, // 10分钟 - 垃圾回收时间
// 网络重试策略
retry: (failureCount, error) => {
// 对于认证错误不重试
if (error instanceof Error && error.message.includes('401')) {
return false;
}
// 最多重试2次
return failureCount < 2;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// 重新获取策略
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
},
mutations: {
// Mutation 重试策略
retry: 1,
retryDelay: 1000,
},
},
});
}
2. 在应用中集成
在根组件中使用 QueryClientProvider:
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import { createQueryClient } from '@/lib/query-client'
function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => createQueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
Query Keys 管理
我们使用统一的 Query Keys 管理策略,所有查询键都集中在 src/modules/shared/lib/api-hooks.ts
中:
export const queryKeys = {
profile: () => ["profile"] as const,
projects: (params?: { userId?: string }) =>
params ? ["projects", params] : ["projects"] as const,
notifications: {
all: () => ["notifications"] as const,
list: (page: number, limit: number) => ["notifications", "list", page, limit] as const,
unreadCount: () => ["notifications", "unread-count"] as const,
},
bookmarks: {
users: () => ["bookmarks", "users"] as const,
projects: () => ["bookmarks", "projects"] as const,
events: () => ["bookmarks", "events"] as const,
},
user: {
registrations: () => ["user", "registrations"] as const,
events: () => ["user", "events"] as const,
interactiveUsers: (limit?: number) =>
limit ? ["user", "interactive-users", limit] : ["user", "interactive-users"] as const,
},
} as const;
使用 as const
确保 TypeScript 类型推断的准确性,这样可以获得更好的类型安全。
基本用法
1. useQuery - 数据查询
useQuery
用于获取和缓存数据:
// 基础用法
export function useProfileQuery() {
return useQuery({
queryKey: queryKeys.profile(),
queryFn: async () => {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("Failed to fetch profile");
}
const data = await response.json();
return data.user;
},
staleTime: 5 * 60 * 1000, // 5分钟
gcTime: 10 * 60 * 1000, // 10分钟
});
}
// 在组件中使用
function ProfileComponent() {
const { data: profile, isLoading, error, refetch } = useProfileQuery();
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<div>
<h1>{profile?.name}</h1>
<button onClick={() => refetch()}>刷新</button>
</div>
);
}
2. 带参数的查询
// 带参数的查询
export function useProjectsQuery(userId?: string) {
return useQuery({
queryKey: queryKeys.projects(userId ? { userId } : undefined),
queryFn: async () => {
const url = userId ? `/api/projects?userId=${userId}` : "/api/projects";
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch projects");
}
const data = await response.json();
return data.projects || [];
},
staleTime: 3 * 60 * 1000, // 3分钟
});
}
// 使用
function ProjectsList({ userId }: { userId?: string }) {
const { data: projects = [] } = useProjectsQuery(userId);
return (
<div>
{projects.map(project => (
<div key={project.id}>{project.title}</div>
))}
</div>
);
}
3. useMutation - 数据变更
useMutation
用于创建、更新、删除数据:
export function useMarkNotificationAsReadMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (notificationId: string) => {
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: "POST",
});
if (!response.ok) {
throw new Error("Failed to mark notification as read");
}
return response.json();
},
onSuccess: () => {
// 更新相关查询的缓存
queryClient.invalidateQueries({ queryKey: queryKeys.notifications.all() });
queryClient.invalidateQueries({ queryKey: queryKeys.notifications.unreadCount() });
},
});
}
// 在组件中使用
function NotificationItem({ notification }: { notification: Notification }) {
const markAsReadMutation = useMarkNotificationAsReadMutation();
const handleMarkAsRead = () => {
markAsReadMutation.mutate(notification.id, {
onSuccess: () => {
console.log('通知已标记为已读');
},
onError: (error) => {
console.error('标记失败:', error);
}
});
};
return (
<div>
<p>{notification.message}</p>
<button
onClick={handleMarkAsRead}
disabled={markAsReadMutation.isPending}
>
{markAsReadMutation.isPending ? '标记中...' : '标记为已读'}
</button>
</div>
);
}
与 Hono.js API 集成
我们的后端使用 Hono.js 构建 RESTful API,所有 API 路由都在 src/server/routes/
目录下。
1. API 路由结构
// src/server/app.ts - Hono 应用主文件
export const app = new Hono().basePath("/api");
// 挂载各种路由器
app.route("/", authRouter)
.route("/", healthRouter)
.route("/events", eventsRouter)
.route("/user", userRouter)
.route("/notifications", notificationsRouter)
// ... 更多路由
2. 活动 API 集成示例
以活动 API 为例,展示完整的集成流程:
// 后端 API (src/server/routes/events.ts)
// GET /api/events - 获取活动列表
app.get("/", zValidator("query", getEventsSchema), async (c) => {
const params = c.req.valid("query");
const result = await getEvents(params);
return c.json({
success: true,
data: result,
});
});
// POST /api/events - 创建新活动
app.post("/", zValidator("json", eventSchema), async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return c.json({ success: false, error: "Authentication required" }, 401);
}
const data = c.req.valid("json");
const event = await createEvent({ ...data, organizerId: session.user.id });
return c.json({ success: true, data: event });
});
// 前端 Query Hooks
export const queryKeys = {
events: {
all: () => ["events"] as const,
list: (params: EventsParams) => ["events", "list", params] as const,
detail: (id: string) => ["events", "detail", id] as const,
},
} as const;
// 获取活动列表
export function useEventsQuery(params: EventsParams = {}) {
return useQuery({
queryKey: queryKeys.events.list(params),
queryFn: async () => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.append(key, String(value));
}
});
const response = await fetch(`/api/events?${searchParams}`);
if (!response.ok) {
throw new Error("Failed to fetch events");
}
const data = await response.json();
return data.data;
},
staleTime: 5 * 60 * 1000, // 5分钟缓存
});
}
// 创建活动
export function useCreateEventMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (eventData: CreateEventInput) => {
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create event');
}
return response.json();
},
onSuccess: () => {
// 创建成功后使相关查询失效
queryClient.invalidateQueries({ queryKey: queryKeys.events.all() });
},
});
}
3. 在组件中使用
function EventsList() {
const [params, setParams] = useState<EventsParams>({
page: 1,
limit: 20,
type: 'HACKATHON'
});
const {
data: eventsData,
isLoading,
error,
refetch
} = useEventsQuery(params);
const createEventMutation = useCreateEventMutation();
const handleCreateEvent = (eventData: CreateEventInput) => {
createEventMutation.mutate(eventData, {
onSuccess: () => {
toast.success('活动创建成功!');
refetch(); // 可选:手动刷新列表
},
onError: (error) => {
toast.error(`创建失败: ${error.message}`);
}
});
};
if (isLoading) return <div>加载活动列表...</div>;
if (error) return <div>加载错误: {error.message}</div>;
return (
<div>
<div className="events-grid">
{eventsData?.events?.map(event => (
<EventCard key={event.id} event={event} />
))}
</div>
<CreateEventButton
onSubmit={handleCreateEvent}
isLoading={createEventMutation.isPending}
/>
</div>
);
}
高级功能
1. 条件查询
function useUserProfile(userId?: string) {
return useQuery({
queryKey: ["user", "profile", userId],
queryFn: () => fetchUserProfile(userId!),
enabled: !!userId, // 只有当 userId 存在时才执行查询
});
}
2. 乐观更新
export function useToggleEventLikeMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ eventId, isLiked }: { eventId: string; isLiked: boolean }) => {
const response = await fetch(`/api/events/${eventId}/like`, {
method: isLiked ? 'DELETE' : 'POST',
});
if (!response.ok) throw new Error('操作失败');
return response.json();
},
onMutate: async ({ eventId, isLiked }) => {
// 取消相关的查询以避免覆盖乐观更新
await queryClient.cancelQueries({ queryKey: queryKeys.events.detail(eventId) });
// 获取当前数据快照
const previousEvent = queryClient.getQueryData(queryKeys.events.detail(eventId));
// 乐观更新
queryClient.setQueryData(queryKeys.events.detail(eventId), (old: any) => ({
...old,
isLiked: !isLiked,
likesCount: isLiked ? old.likesCount - 1 : old.likesCount + 1,
}));
// 返回上下文对象,包含撤销信息
return { previousEvent, eventId };
},
onError: (err, variables, context) => {
// 如果出错,回滚到之前的状态
if (context?.previousEvent) {
queryClient.setQueryData(
queryKeys.events.detail(context.eventId),
context.previousEvent
);
}
},
onSettled: (data, error, variables) => {
// 无论成功还是失败,都重新获取数据确保数据一致性
queryClient.invalidateQueries({
queryKey: queryKeys.events.detail(variables.eventId)
});
},
});
}
3. 无限查询(分页加载)
export function useInfiniteNotifications() {
return useInfiniteQuery({
queryKey: queryKeys.notifications.all(),
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/api/notifications?page=${pageParam}&limit=20`);
if (!response.ok) throw new Error('Failed to fetch notifications');
return response.json();
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => {
// 如果还有更多数据,返回下一页页码
if (lastPage.data.hasNextPage) {
return pages.length + 1;
}
return undefined; // 没有更多数据
},
});
}
// 在组件中使用无限查询
function InfiniteNotificationsList() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteNotifications();
if (status === 'pending') return <div>加载中...</div>;
if (status === 'error') return <div>错误: {error.message}</div>;
return (
<div>
{data.pages.map((group, i) => (
<div key={i}>
{group.data.notifications.map(notification => (
<NotificationItem key={notification.id} notification={notification} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? '加载中...'
: hasNextPage
? '加载更多'
: '没有更多了'}
</button>
</div>
);
}
4. 并行查询
function UserDashboard({ userId }: { userId: string }) {
// 多个查询并行执行
const profileQuery = useProfileQuery();
const projectsQuery = useProjectsQuery(userId);
const notificationsQuery = useUnreadNotificationsCountQuery();
// 检查所有查询的状态
const isLoading = profileQuery.isLoading || projectsQuery.isLoading || notificationsQuery.isLoading;
const hasError = profileQuery.error || projectsQuery.error || notificationsQuery.error;
if (isLoading) return <div>加载用户数据...</div>;
if (hasError) return <div>加载出错</div>;
return (
<div>
<UserProfile profile={profileQuery.data} />
<ProjectsList projects={projectsQuery.data} />
<NotificationBadge count={notificationsQuery.data} />
</div>
);
}
最佳实践
1. 错误处理
// 全局错误处理
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
// 不重试认证错误
if (error?.status === 401) return false;
// 不重试客户端错误(4xx)
if (error?.status >= 400 && error?.status < 500) return false;
// 其他错误最多重试2次
return failureCount < 2;
},
},
},
});
// 组件级错误处理
function ComponentWithErrorHandling() {
const { data, error, isError } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
if (isError) {
// 根据错误类型显示不同的错误信息
if (error.message.includes('401')) {
return <div>请先登录</div>;
}
if (error.message.includes('403')) {
return <div>权限不足</div>;
}
return <div>加载失败: {error.message}</div>;
}
return <div>{/* 渲染数据 */}</div>;
}
2. 加载状态管理
function SmartLoadingComponent() {
const { data, isLoading, isFetching, isPreviousData } = useQuery({
queryKey: ['data', page],
queryFn: () => fetchData(page),
keepPreviousData: true, // 保持之前的数据直到新数据加载完成
});
return (
<div>
{/* 首次加载显示骨架屏 */}
{isLoading && <SkeletonLoader />}
{/* 有数据时显示内容 */}
{data && (
<div>
{/* 数据内容 */}
<DataContent data={data} />
{/* 后台刷新指示器 */}
{isFetching && !isLoading && (
<div className="refresh-indicator">刷新中...</div>
)}
{/* 分页加载时显示之前的数据 */}
{isPreviousData && (
<div className="loading-overlay">加载新页面...</div>
)}
</div>
)}
</div>
);
}
3. 查询依赖
// 依赖查询:只有当用户信息加载完成后才获取用户的项目
function UserProjectsPage() {
const { data: user } = useProfileQuery();
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchUserProjects(user!.id),
enabled: !!user?.id, // 只有当用户ID存在时才执行查询
});
return <div>{/* 渲染内容 */}</div>;
}
4. 缓存管理
// 手动更新缓存
function updateCacheExample() {
const queryClient = useQueryClient();
// 设置查询数据
queryClient.setQueryData(['user', userId], newUserData);
// 使查询失效(触发重新获取)
queryClient.invalidateQueries({ queryKey: ['events'] });
// 预获取数据
queryClient.prefetchQuery({
queryKey: ['events', { page: 2 }],
queryFn: () => fetchEvents({ page: 2 }),
});
// 移除查询数据
queryClient.removeQueries({ queryKey: ['temp-data'] });
}
调试工具
在开发环境中启用 React Query DevTools:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
确保只在开发环境中包含 DevTools,避免在生产环境中暴露调试信息。
性能优化
1. 选择性订阅
// 只订阅需要的数据字段
const { data: userName } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
select: (data) => data.name, // 只选择用户名
});
2. 查询键工厂函数
// 使用工厂函数生成复杂的查询键
export const createQueryKeyWithParams = (
key: string | string[],
params: Record<string, string | number>,
) => {
return [
...(Array.isArray(key) ? key : [key]),
Object.entries(params)
.reduce((acc, [key, value]) => {
acc.push(`${key}:${value}`);
return acc;
}, [] as string[])
.join("_"),
] as const;
};
// 使用
const queryKey = createQueryKeyWithParams("events", {
type: "HACKATHON",
organizationId: "123"
});
总结
TanStack Query 为我们的应用提供了强大的数据管理能力:
- 与 Hono.js API 完美集成:通过统一的错误处理和响应格式
- 智能缓存策略:减少不必要的网络请求
- 优秀的用户体验:加载状态、错误处理、乐观更新
- 类型安全:配合 TypeScript 提供完整的类型推断
- 可维护性:集中化的查询键管理和复用的 Hook
通过遵循这些最佳实践,你可以构建出高性能、用户体验良好的现代 React 应用程序。