tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query (React Query) Skill
TanStack Query (React Query) 技能指南
Summary
概述
TanStack Query (formerly React Query) is a powerful asynchronous state management library for React that handles server-state fetching, caching, synchronization, and updates. It eliminates the need for manual data fetching boilerplate and provides built-in features like background refetching, optimistic updates, pagination, and intelligent cache management.
TanStack Query(前身为React Query)是一款功能强大的React异步状态管理库,用于处理服务端状态的获取、缓存、同步和更新。它消除了手动编写数据获取样板代码的需求,并提供了后台重新获取、乐观更新、分页和智能缓存管理等内置功能。
When to Use
适用场景
Use TanStack Query when:
- Fetching data from REST APIs, GraphQL, or tRPC endpoints
- Need automatic background refetching and cache invalidation
- Building real-time dashboards with polling or websocket data
- Implementing infinite scroll or pagination
- Require optimistic UI updates for mutations
- Managing complex server-state synchronization
- Need offline support with cache persistence
- Building applications with frequent data updates
TanStack Query excels at:
- Server-state management (API data, external state)
- Request deduplication and caching
- Stale-while-revalidate patterns
- Loading and error state management
- Prefetching and eager loading
- Parallel and dependent query orchestration
Avoid TanStack Query for:
- Pure client-side state (use Zustand, Jotai, or Context)
- Form state management (use React Hook Form, Formik)
- Simple one-time fetches without caching needs
在以下场景中使用TanStack Query:
- 从REST API、GraphQL或tRPC端点获取数据
- 需要自动后台重新获取和缓存失效机制
- 构建轮询或基于WebSocket数据的实时仪表盘
- 实现无限滚动或分页功能
- 需要针对变更操作的乐观UI更新
- 管理复杂的服务端状态同步
- 需要带缓存持久化的离线支持
- 构建数据频繁更新的应用
TanStack Query的优势场景:
- 服务端状态管理(API数据、外部状态)
- 请求去重与缓存
- 过期时重新验证(Stale-while-revalidate)模式
- 加载与错误状态管理
- 预获取与提前加载
- 并行与依赖查询编排
避免在以下场景使用TanStack Query:
- 纯客户端状态(使用Zustand、Jotai或Context)
- 表单状态管理(使用React Hook Form、Formik)
- 无需缓存的简单一次性数据获取
Quick Start
快速开始
Installation
安装
bash
npm install @tanstack/react-querybash
npm install @tanstack/react-queryDevTools (optional but recommended)
DevTools(可选但推荐)
npm install @tanstack/react-query-devtools
undefinednpm install @tanstack/react-query-devtools
undefinedBasic Setup
基础配置
tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分钟
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}First Query
第一个查询
tsx
// components/UserProfile.tsx
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json() as Promise<User>;
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}tsx
// components/UserProfile.tsx
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('获取用户失败');
return response.json() as Promise<User>;
},
});
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误:{error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}First Mutation
第一个变更
tsx
// components/CreateUserForm.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser: { name: string; email: string }) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
</form>
);
}tsx
// components/CreateUserForm.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser: { name: string; email: string }) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// 失效并重新获取用户列表
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '创建中...' : '创建用户'}
</button>
{mutation.isError && <p>错误:{mutation.error.message}</p>}
</form>
);
}Core Concepts
核心概念
Server State vs Client State
服务端状态 vs 客户端状态
Server State Characteristics:
- Persisted remotely (database, API, cloud)
- Requires asynchronous APIs for fetching/updating
- Can be out of sync with client
- Can be updated by other users/systems
- Examples: User data, posts, products, settings
Client State Characteristics:
- Persisted locally (memory, localStorage)
- Synchronously accessible
- Fully controlled by client
- Examples: UI theme, modal open/closed, form inputs
TanStack Query manages server state. Use Zustand/Context for client state.
服务端状态特征:
- 远程持久化(数据库、API、云端)
- 需要异步API进行获取/更新
- 可能与客户端状态不同步
- 可被其他用户/系统更新
- 示例:用户数据、帖子、商品、设置
客户端状态特征:
- 本地持久化(内存、localStorage)
- 可同步访问
- 完全由客户端控制
- 示例:UI主题、模态框开关、表单输入
TanStack Query专注于管理服务端状态,客户端状态请使用Zustand/Context。
Query Keys
查询键(Query Keys)
Query keys uniquely identify queries and their cached data.
Key Structure:
tsx
// String key (simple)
queryKey: ['todos']
// Array key (recommended for dependencies)
queryKey: ['todo', todoId]
queryKey: ['todos', { status: 'active', page: 1 }]
// Nested arrays (complex hierarchies)
queryKey: ['users', userId, 'posts', { sort: 'date' }]Key Matching:
tsx
// Exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true });
// Prefix match (invalidates all matching)
queryClient.invalidateQueries({ queryKey: ['todos'] }); // Matches ['todos', 1], ['todos', 2], etc.
// Predicate match
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'todos' && query.state.data?.status === 'draft'
});Best Practices:
- Use arrays with hierarchical structure:
['resource', id, 'subresource'] - Place variables at the end:
['users', { filter, sort }] - Consistent ordering across components
- Use objects for complex parameters
查询键用于唯一标识查询及其缓存数据。
键结构:
tsx
// 字符串键(简单场景)
queryKey: ['todos']
// 数组键(推荐用于带依赖的场景)
queryKey: ['todo', todoId]
queryKey: ['todos', { status: 'active', page: 1 }]
// 嵌套数组(复杂层级)
queryKey: ['users', userId, 'posts', { sort: 'date' }]键匹配:
tsx
// 精确匹配
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true });
// 前缀匹配(失效所有匹配项)
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 匹配 ['todos', 1], ['todos', 2] 等
// 谓词匹配
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'todos' && query.state.data?.status === 'draft'
});最佳实践:
- 使用层级结构的数组:
['资源', id, '子资源'] - 将变量放在末尾:
['users', { filter, sort }] - 在组件间保持一致的顺序
- 复杂参数使用对象
Query Lifecycle
查询生命周期
FRESH → STALE → INACTIVE → GARBAGE COLLECTED
↓ ↓ ↓ ↓
0ms staleTime no observers cacheTimeStates:
- Fresh: Data is considered up-to-date (within )
staleTime - Stale: Data might be outdated, will refetch on trigger
- Inactive: No components using the query
- Garbage Collected: Removed from cache after
cacheTime
Configuration:
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // 5 minutes (data fresh)
gcTime: 10 * 60 * 1000, // 10 minutes (cache retention)
refetchOnWindowFocus: true, // Refetch when window regains focus
refetchOnReconnect: true, // Refetch when reconnecting
refetchInterval: 30000, // Poll every 30 seconds
});新鲜(FRESH)→ 过期(STALE)→ 非活跃(INACTIVE)→ 垃圾回收(GARBAGE COLLECTED)
↓ ↓ ↓ ↓
0ms staleTime 无观察者 cacheTime状态说明:
- 新鲜:数据被认为是最新的(在有效期内)
staleTime - 过期:数据可能已过时,触发时会重新获取
- 非活跃:没有组件在使用该查询
- 垃圾回收:在后从缓存中移除
cacheTime
配置示例:
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // 5分钟(数据新鲜期)
gcTime: 10 * 60 * 1000, // 10分钟(缓存保留时间)
refetchOnWindowFocus: true, // 窗口重新获得焦点时重新获取
refetchOnReconnect: true, // 重新连接网络时重新获取
refetchInterval: 30000, // 每30秒轮询一次
});Cache Behavior
缓存行为
Automatic Caching:
tsx
// First component - triggers fetch
function ComponentA() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.name}</div>;
}
// Second component - uses cache instantly
function ComponentB() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.email}</div>; // No second fetch!
}Stale-While-Revalidate:
tsx
// Shows cached data immediately, refetches in background if stale
const { data, isRefetching } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 60000, // Fresh for 1 minute
});
// data available from cache immediately
// isRefetching = true if background refetch happening自动缓存:
tsx
// 第一个组件 - 触发请求
function ComponentA() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.name}</div>;
}
// 第二个组件 - 立即使用缓存
function ComponentB() {
const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
return <div>{data?.email}</div>; // 不会发起第二次请求!
}过期时重新验证(Stale-While-Revalidate):
tsx
// 立即显示缓存数据,如果数据过期则在后台重新获取
const { data, isRefetching } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 60000, // 1分钟内数据新鲜
});
// data立即从缓存获取
// isRefetching = true 表示后台正在重新获取Queries
查询(Queries)
useQuery Hook
useQuery Hook
Basic Syntax:
tsx
const {
data, // Query result
error, // Error object if failed
isLoading, // First load (no cached data)
isFetching, // Any fetch (including background)
isSuccess, // Query succeeded
isError, // Query failed
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'fetching' | 'paused' | 'idle'
refetch, // Manual refetch function
} = useQuery({
queryKey: ['key'],
queryFn: async () => { /* fetch logic */ },
});基础语法:
tsx
const {
data, // 查询结果
error, // 请求失败时的错误对象
isLoading, // 首次加载(无缓存数据)
isFetching, // 任何请求中(包括后台)
isSuccess, // 查询成功
isError, // 查询失败
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'fetching' | 'paused' | 'idle'
refetch, // 手动重新获取函数
} = useQuery({
queryKey: ['key'],
queryFn: async () => { /* 请求逻辑 */ },
});Query Function Patterns
查询函数模式
Basic Fetch:
tsx
const { data } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Network error');
return response.json();
},
});Query Key in Function:
tsx
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ queryKey }) => {
const [_key, userId] = queryKey;
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});Abort Signal (Cancellation):
tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal });
return response.json();
},
});
// Automatically cancels on unmount or when query becomes inactiveAxios Pattern:
tsx
import axios from 'axios';
const { data } = useQuery({
queryKey: ['repos', username],
queryFn: ({ signal }) =>
axios.get(`/api/repos/${username}`, { signal }).then(res => res.data),
});基础请求:
tsx
const { data } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('网络错误');
return response.json();
},
});查询键在函数中使用:
tsx
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ queryKey }) => {
const [_key, userId] = queryKey;
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});中止信号(取消请求):
tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal });
return response.json();
},
});
// 组件卸载或查询变为非活跃时自动取消请求Axios模式:
tsx
import axios from 'axios';
const { data } = useQuery({
queryKey: ['repos', username],
queryFn: ({ signal }) =>
axios.get(`/api/repos/${username}`, { signal }).then(res => res.data),
});Dependent Queries
依赖查询
Sequential Queries:
tsx
// Wait for user before fetching projects
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user, // Only run when user exists
});Conditional Queries:
tsx
const { data } = useQuery({
queryKey: ['premium-features', userId],
queryFn: fetchPremiumFeatures,
enabled: user?.isPremium === true, // Only fetch for premium users
});顺序查询:
tsx
// 等待用户数据获取完成后再获取项目
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user, // 仅当user存在时执行
});条件查询:
tsx
const { data } = useQuery({
queryKey: ['premium-features', userId],
queryFn: fetchPremiumFeatures,
enabled: user?.isPremium === true, // 仅为付费用户获取
});Parallel Queries
并行查询
Manual Parallel:
tsx
function Dashboard() {
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const projects = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });
if (users.isLoading || posts.isLoading || projects.isLoading) {
return <Spinner />;
}
return <div>/* render dashboard */</div>;
}useQueries (Dynamic Parallel):
tsx
import { useQueries } from '@tanstack/react-query';
function MultiUserProfiles({ userIds }: { userIds: number[] }) {
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 60000,
})),
});
const allLoaded = results.every(r => r.isSuccess);
if (!allLoaded) return <Spinner />;
return (
<div>
{results.map((result, i) => (
<UserCard key={userIds[i]} user={result.data} />
))}
</div>
);
}手动并行:
tsx
function Dashboard() {
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const projects = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });
if (users.isLoading || posts.isLoading || projects.isLoading) {
return <Spinner />;
}
return <div>/* 渲染仪表盘 */</div>;
}useQueries(动态并行):
tsx
import { useQueries } from '@tanstack/react-query';
function MultiUserProfiles({ userIds }: { userIds: number[] }) {
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 60000,
})),
});
const allLoaded = results.every(r => r.isSuccess);
if (!allLoaded) return <Spinner />;
return (
<div>
{results.map((result, i) => (
<UserCard key={userIds[i]} user={result.data} />
))}
</div>
);
}Query Placeholders
查询占位符
Placeholder Data (Instant UI):
tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
placeholderData: [], // Show empty array while loading
});
// Dynamic placeholder from cache
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
placeholderData: () => {
// Use cached list to find placeholder
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
});Initial Data (Hydration):
tsx
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
initialData: () => {
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
initialDataUpdatedAt: () =>
queryClient.getQueryState(['todos'])?.dataUpdatedAt,
});Difference:
- : Not persisted to cache, purely UI
placeholderData - : Persisted to cache as real data
initialData
占位符数据(即时UI):
tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
placeholderData: [], // 加载时显示空数组
});
// 从缓存中动态获取占位符数据
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
placeholderData: () => {
// 使用缓存的列表数据查找占位符
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
});初始数据(水合):
tsx
const { data } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
initialData: () => {
return queryClient
.getQueryData(['todos'])
?.find(d => d.id === id);
},
initialDataUpdatedAt: () =>
queryClient.getQueryState(['todos'])?.dataUpdatedAt,
});区别:
- :不持久化到缓存,仅用于UI展示
placeholderData - :作为真实数据持久化到缓存
initialData
Mutations
变更(Mutations)
useMutation Hook
useMutation Hook
Basic Mutation:
tsx
const mutation = useMutation({
mutationFn: async (newTodo: Todo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return response.json();
},
onSuccess: (data) => {
console.log('Created:', data);
},
onError: (error) => {
console.error('Failed:', error);
},
});
// Trigger mutation
mutation.mutate({ title: 'New Todo', done: false });
// Async/await variant
try {
const data = await mutation.mutateAsync(newTodo);
console.log(data);
} catch (error) {
console.error(error);
}Mutation State:
tsx
const {
mutate, // Trigger function
mutateAsync, // Promise variant
data, // Result from successful mutation
error, // Error from failed mutation
isPending, // Mutation in progress
isSuccess, // Mutation succeeded
isError, // Mutation failed
reset, // Reset mutation state
} = useMutation({ /* ... */ });基础变更:
tsx
const mutation = useMutation({
mutationFn: async (newTodo: Todo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return response.json();
},
onSuccess: (data) => {
console.log('已创建:', data);
},
onError: (error) => {
console.error('创建失败:', error);
},
});
// 触发变更
mutation.mutate({ title: '新待办', done: false });
// Async/await 变体
try {
const data = await mutation.mutateAsync(newTodo);
console.log(data);
} catch (error) {
console.error(error);
}变更状态:
tsx
const {
mutate, // 触发函数
mutateAsync, // Promise变体
data, // 变更成功后的结果
error, // 变更失败后的错误
isPending, // 变更进行中
isSuccess, // 变更成功
isError, // 变更失败
reset, // 重置变更状态
} = useMutation({ /* ... */ });Cache Invalidation
缓存失效
Invalidate Queries After Mutation:
tsx
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Refetch all 'todos' queries
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});Multiple Invalidations:
tsx
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// Invalidate multiple query families
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['teams', data.teamId] });
},
});Selective Invalidation:
tsx
// Only invalidate specific queries
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // Only ['todos'], not ['todos', 1]
});
// Predicate-based invalidation
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.status === 'draft',
});变更后失效查询:
tsx
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// 重新获取所有'todos'查询
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});多查询失效:
tsx
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// 失效多个查询家族
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['teams', data.teamId] });
},
});选择性失效:
tsx
// 仅失效特定查询
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // 仅匹配 ['todos'],不匹配 ['todos', 1]
});
// 基于谓词的失效
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.status === 'draft',
});Manual Cache Updates
手动缓存更新
setQueryData (Direct Update):
tsx
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: (updatedTodo) => {
// Update specific todo in cache
queryClient.setQueryData(
['todo', updatedTodo.id],
updatedTodo
);
// Update todo in list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo =>
todo.id === updatedTodo.id ? updatedTodo : todo
)
);
},
});Immutable Updates:
tsx
// Add to list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
[...old, newTodo]
);
// Remove from list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.filter(todo => todo.id !== deletedId)
);
// Update in list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === id ? { ...todo, ...updates } : todo)
);setQueryData(直接更新):
tsx
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: (updatedTodo) => {
// 更新缓存中的特定待办项
queryClient.setQueryData(
['todo', updatedTodo.id],
updatedTodo
);
// 更新列表中的待办项
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo =>
todo.id === updatedTodo.id ? updatedTodo : todo
)
);
},
});不可变更新:
tsx
// 添加到列表
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
[...old, newTodo]
);
// 从列表移除
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.filter(todo => todo.id !== deletedId)
);
// 更新列表中的项
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === id ? { ...todo, ...updates } : todo)
);Optimistic Updates
乐观更新
Basic Optimistic Update
基础乐观更新
tsx
const mutation = useMutation({
mutationFn: updateTodo,
// Before mutation executes
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update cache
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
// Return context with snapshot
return { previousTodos };
},
// On error, rollback
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// Always refetch after success or error
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});tsx
const mutation = useMutation({
mutationFn: updateTodo,
// 变更执行前
onMutate: async (newTodo) => {
// 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 快照之前的值
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新缓存
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
// 返回包含快照的上下文
return { previousTodos };
},
// 错误时回滚
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 成功或错误后都重新获取
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});Complex Optimistic Update Pattern
复杂乐观更新模式
tsx
interface Todo {
id: number;
title: string;
done: boolean;
}
const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (updatedTodo: Todo) => {
const response = await fetch(`/api/todos/${updatedTodo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTodo),
});
if (!response.ok) throw new Error('Update failed');
return response.json();
},
onMutate: async (updatedTodo) => {
// Cancel queries to prevent race conditions
await queryClient.cancelQueries({ queryKey: ['todos'] });
await queryClient.cancelQueries({ queryKey: ['todo', updatedTodo.id] });
// Snapshot current state
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
const previousTodo = queryClient.getQueryData<Todo>(['todo', updatedTodo.id]);
// Optimistically update list
if (previousTodos) {
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
);
}
// Optimistically update detail
queryClient.setQueryData(['todo', updatedTodo.id], updatedTodo);
return { previousTodos, previousTodo };
},
onError: (err, updatedTodo, context) => {
// Rollback on error
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
if (context?.previousTodo) {
queryClient.setQueryData(['todo', updatedTodo.id], context.previousTodo);
}
},
onSettled: (data, error, variables) => {
// Always refetch to ensure sync
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todo', variables.id] });
},
});
};
// Usage
function TodoItem({ todo }: { todo: Todo }) {
const updateTodo = useUpdateTodo();
const toggleDone = () => {
updateTodo.mutate({ ...todo, done: !todo.done });
};
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={toggleDone}
disabled={updateTodo.isPending}
/>
{todo.title}
</div>
);
}tsx
interface Todo {
id: number;
title: string;
done: boolean;
}
const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (updatedTodo: Todo) => {
const response = await fetch(`/api/todos/${updatedTodo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTodo),
});
if (!response.ok) throw new Error('更新失败');
return response.json();
},
onMutate: async (updatedTodo) => {
// 取消查询以防止竞态条件
await queryClient.cancelQueries({ queryKey: ['todos'] });
await queryClient.cancelQueries({ queryKey: ['todo', updatedTodo.id] });
// 快照当前状态
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
const previousTodo = queryClient.getQueryData<Todo>(['todo', updatedTodo.id]);
// 乐观更新列表
if (previousTodos) {
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
);
}
// 乐观更新详情
queryClient.setQueryData(['todo', updatedTodo.id], updatedTodo);
return { previousTodos, previousTodo };
},
onError: (err, updatedTodo, context) => {
// 错误时回滚
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
if (context?.previousTodo) {
queryClient.setQueryData(['todo', updatedTodo.id], context.previousTodo);
}
},
onSettled: (data, error, variables) => {
// 始终重新获取以确保同步
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todo', variables.id] });
},
});
};
// 使用示例
function TodoItem({ todo }: { todo: Todo }) {
const updateTodo = useUpdateTodo();
const toggleDone = () => {
updateTodo.mutate({ ...todo, done: !todo.done });
};
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={toggleDone}
disabled={updateTodo.isPending}
/>
{todo.title}
</div>
);
}Pagination
分页
useInfiniteQuery (Infinite Scroll)
useInfiniteQuery(无限滚动)
Basic Infinite Query:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
interface PostsResponse {
posts: Post[];
nextCursor?: number;
}
function InfinitePosts() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json() as Promise<PostsResponse>;
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
);
}Bi-directional Pagination:
tsx
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json();
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
initialPageParam: 0,
});Infinite Scroll with Intersection Observer:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
function AutoLoadPosts() {
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
// Auto-fetch when sentinel comes into view
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
))}
{/* Sentinel element */}
<div ref={ref}>
{isFetchingNextPage && <Spinner />}
</div>
</div>
);
}基础无限查询:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
interface PostsResponse {
posts: Post[];
nextCursor?: number;
}
function InfinitePosts() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json() as Promise<PostsResponse>;
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? '加载更多中...'
: hasNextPage
? '加载更多'
: '没有更多内容了'}
</button>
</div>
);
}双向分页:
tsx
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?cursor=${pageParam}`);
return response.json();
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
initialPageParam: 0,
});结合Intersection Observer的无限滚动:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
function AutoLoadPosts() {
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
});
// 当哨兵元素进入视图时自动获取下一页
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
))}
{/* 哨兵元素 */}
<div ref={ref}>
{isFetchingNextPage && <Spinner />}
</div>
</div>
);
}Traditional Pagination
传统分页
Page-Based Pagination:
tsx
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: (previousData) => previousData, // Keep previous data while loading
});
return (
<div>
{isLoading ? (
<Spinner />
) : (
<div>
{data.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
)}
<div>
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => setPage(old => old + 1)}
disabled={!data?.hasMore}
>
Next
</button>
</div>
</div>
);
}Prefetch Next Page:
tsx
function PaginatedPosts() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
});
// Prefetch next page
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}
}, [data, page, queryClient]);
return (
<div>
{/* ... */}
</div>
);
}基于页码的分页:
tsx
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: (previousData) => previousData, // 加载时保留之前的数据
});
return (
<div>
{isLoading ? (
<Spinner />
) : (
<div>
{data.posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
)}
<div>
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
上一页
</button>
<span>第 {page} 页</span>
<button
onClick={() => setPage(old => old + 1)}
disabled={!data?.hasMore}
>
下一页
</button>
</div>
</div>
);
}预获取下一页:
tsx
function PaginatedPosts() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
});
// 预获取下一页
useEffect(() => {
if (data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}
}, [data, page, queryClient]);
return (
<div>
{/* ... */}
</div>
);
}Cache Management
缓存管理
Query Client Methods
查询客户端方法
getQueryData (Read Cache):
tsx
const todos = queryClient.getQueryData<Todo[]>(['todos']);
const user = queryClient.getQueryData<User>(['user', userId]);setQueryData (Write Cache):
tsx
queryClient.setQueryData(['user', 1], newUser);
// Updater function
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [...old, newTodo]);invalidateQueries (Mark Stale + Refetch):
tsx
// Invalidate all queries
queryClient.invalidateQueries();
// Invalidate by key prefix
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Exact match only
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// With refetch control
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
});refetchQueries (Immediate Refetch):
tsx
// Refetch all active queries
await queryClient.refetchQueries();
// Refetch specific queries
await queryClient.refetchQueries({ queryKey: ['todos'] });
// Refetch with filters
await queryClient.refetchQueries({
queryKey: ['todos'],
type: 'active', // Only refetch active queries
});removeQueries (Delete from Cache):
tsx
// Remove all queries
queryClient.removeQueries();
// Remove specific
queryClient.removeQueries({ queryKey: ['todos', 1] });
// Remove with predicate
queryClient.removeQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.isArchived === true,
});resetQueries (Reset to Initial State):
tsx
// Reset all queries
queryClient.resetQueries();
// Reset specific
queryClient.resetQueries({ queryKey: ['todos'] });getQueryData(读取缓存):
tsx
const todos = queryClient.getQueryData<Todo[]>(['todos']);
const user = queryClient.getQueryData<User>(['user', userId]);setQueryData(写入缓存):
tsx
queryClient.setQueryData(['user', 1], newUser);
// 更新器函数
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [...old, newTodo]);invalidateQueries(标记为过期 + 重新获取):
tsx
// 失效所有查询
queryClient.invalidateQueries();
// 按键前缀失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 仅精确匹配
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });
// 带重新获取控制
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
});refetchQueries(立即重新获取):
tsx
// 重新获取所有活跃查询
await queryClient.refetchQueries();
// 重新获取特定查询
await queryClient.refetchQueries({ queryKey: ['todos'] });
// 带过滤器重新获取
await queryClient.refetchQueries({
queryKey: ['todos'],
type: 'active', // 仅重新获取活跃查询
});removeQueries(从缓存中删除):
tsx
// 删除所有查询
queryClient.removeQueries();
// 删除特定查询
queryClient.removeQueries({ queryKey: ['todos', 1] });
// 按谓词删除
queryClient.removeQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' &&
query.state.data?.isArchived === true,
});resetQueries(重置为初始状态):
tsx
// 重置所有查询
queryClient.resetQueries();
// 重置特定查询
queryClient.resetQueries({ queryKey: ['todos'] });Cache Configuration
缓存配置
Global Defaults:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
refetchOnWindowFocus: false,
refetchOnReconnect: true,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
mutations: {
retry: 1,
},
},
});Per-Query Configuration:
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: Infinity, // Never mark stale
gcTime: Infinity, // Never garbage collect
refetchInterval: 5000, // Refetch every 5s
refetchIntervalInBackground: false, // Don't refetch when tab inactive
});全局默认值:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分钟
gcTime: 5 * 60 * 1000, // 5分钟(原cacheTime)
refetchOnWindowFocus: false,
refetchOnReconnect: true,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
mutations: {
retry: 1,
},
},
});单查询配置:
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: Infinity, // 永不标记为过期
gcTime: Infinity, // 永不垃圾回收
refetchInterval: 5000, // 每5秒重新获取一次
refetchIntervalInBackground: false, // 标签页非活跃时不重新获取
});Cache Persistence
缓存持久化
Persist to LocalStorage:
tsx
import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});IndexedDB Persistence:
tsx
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';
const persister = createAsyncStoragePersister({
storage: {
getItem: async (key) => await get(key),
setItem: async (key, value) => await set(key, value),
removeItem: async (key) => await del(key),
},
});持久化到LocalStorage:
tsx
import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24小时
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24小时
});IndexedDB持久化:
tsx
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';
const persister = createAsyncStoragePersister({
storage: {
getItem: async (key) => await get(key),
setItem: async (key, value) => await set(key, value),
removeItem: async (key) => await del(key),
},
});Error Handling and Retry
错误处理与重试
Error Handling
错误处理
Query Error Boundaries:
tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Component />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
// Component throws errors to boundary
function Component() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
throwOnError: true, // Throw errors to error boundary
});
return <div>{data.name}</div>;
}Custom Error Types:
tsx
class APIError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}
const { error } = useQuery({
queryKey: ['user'],
queryFn: async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new APIError(
'Failed to fetch user',
response.status,
await response.text()
);
}
return response.json();
},
});
if (error instanceof APIError) {
if (error.status === 404) return <NotFound />;
if (error.status === 401) return <Unauthorized />;
}查询错误边界:
tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>错误:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)}
>
<Component />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
// 组件将错误抛出到边界
function Component() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
throwOnError: true, // 将错误抛出到错误边界
});
return <div>{data.name}</div>;
}自定义错误类型:
tsx
class APIError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}
const { error } = useQuery({
queryKey: ['user'],
queryFn: async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new APIError(
'获取用户失败',
response.status,
await response.text()
);
}
return response.json();
},
});
if (error instanceof APIError) {
if (error.status === 404) return <NotFound />;
if (error.status === 401) return <Unauthorized />;
}Retry Logic
重试逻辑
Default Retry:
tsx
// Retries 3 times with exponential backoff
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3, // Number of retries
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});Conditional Retry:
tsx
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: (failureCount, error) => {
// Don't retry on 404
if (error instanceof APIError && error.status === 404) {
return false;
}
// Retry up to 3 times for other errors
return failureCount < 3;
},
});Mutation Retry:
tsx
useMutation({
mutationFn: createUser,
retry: 2, // Retry mutations (use sparingly)
retryDelay: 1000,
});默认重试:
tsx
// 指数退避重试3次
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3, // 重试次数
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});条件重试:
tsx
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: (failureCount, error) => {
// 404错误不重试
if (error instanceof APIError && error.status === 404) {
return false;
}
// 其他错误最多重试3次
return failureCount < 3;
},
});变更重试:
tsx
useMutation({
mutationFn: createUser,
retry: 2, // 变更操作重试(谨慎使用)
retryDelay: 1000,
});Network Status Detection
网络状态检测
Online/Offline Handling:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 'online' | 'always' | 'offlineFirst'
refetchOnReconnect: true,
},
},
});
// Custom online/offline indicator
function OnlineStatus() {
const queryClient = useQueryClient();
const isOnline = useOnlineManager().isOnline();
useEffect(() => {
if (isOnline) {
queryClient.refetchQueries();
}
}, [isOnline, queryClient]);
return isOnline ? <OnlineIcon /> : <OfflineIcon />;
}在线/离线处理:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 'online' | 'always' | 'offlineFirst'
refetchOnReconnect: true,
},
},
});
// 自定义在线/离线指示器
function OnlineStatus() {
const queryClient = useQueryClient();
const isOnline = useOnlineManager().isOnline();
useEffect(() => {
if (isOnline) {
queryClient.refetchQueries();
}
}, [isOnline, queryClient]);
return isOnline ? <OnlineIcon /> : <OfflineIcon />;
}SSR and Hydration
SSR与水合
Next.js App Router
Next.js App Router
Server Component Data Fetching:
tsx
// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { UsersList } from './UsersList';
export default async function UsersPage() {
const queryClient = new QueryClient();
// Prefetch on server
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UsersList />
</HydrationBoundary>
);
}Client Component:
tsx
// app/users/UsersList.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
export function UsersList() {
// Uses hydrated data from server
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}服务端组件数据获取:
tsx
// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { UsersList } from './UsersList';
export default async function UsersPage() {
const queryClient = new QueryClient();
// 服务端预获取
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UsersList />
</HydrationBoundary>
);
}客户端组件:
tsx
// app/users/UsersList.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
export function UsersList() {
// 使用服务端水合的数据
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}Next.js Pages Router
Next.js Pages Router
getServerSideProps:
tsx
import { dehydrate, QueryClient } from '@tanstack/react-query';
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
function UsersPage() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <div>{/* ... */}</div>;
}
export default UsersPage;_app.tsx Setup:
tsx
// pages/_app.tsx
import { useState } from 'react';
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query';
export default function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
);
}getServerSideProps:
tsx
import { dehydrate, QueryClient } from '@tanstack/react-query';
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
function UsersPage() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <div>{/* ... */}</div>;
}
export default UsersPage;_app.tsx配置:
tsx
// pages/_app.tsx
import { useState } from 'react';
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query';
export default function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
);
}Streaming SSR
流式SSR
Suspense Integration:
tsx
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: number }) {
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// No loading state needed - Suspense handles it
return <div>{data.name}</div>;
}
// In parent component
<Suspense fallback={<Spinner />}>
<UserProfile userId={1} />
</Suspense>Suspense集成:
tsx
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: number }) {
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// 无需加载状态 - Suspense处理
return <div>{data.name}</div>;
}
// 在父组件中
<Suspense fallback={<Spinner />}>
<UserProfile userId={1} />
</Suspense>Integration Patterns
集成模式
tRPC Integration
tRPC集成
Setup:
tsx
// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();Provider:
tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '@/utils/trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}Usage:
tsx
function UserProfile() {
// Query
const { data } = trpc.user.getById.useQuery({ id: 1 });
// Mutation
const utils = trpc.useUtils();
const mutation = trpc.user.create.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
},
});
return <div>{data?.name}</div>;
}配置:
tsx
// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();Provider:
tsx
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '@/utils/trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}使用:
tsx
function UserProfile() {
// 查询
const { data } = trpc.user.getById.useQuery({ id: 1 });
// 变更
const utils = trpc.useUtils();
const mutation = trpc.user.create.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
},
});
return <div>{data?.name}</div>;
}REST API with Axios
REST API与Axios
API Client:
tsx
// lib/api-client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Handle unauthorized
window.location.href = '/login';
}
return Promise.reject(error);
}
);Query Hooks:
tsx
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async ({ signal }) => {
const { data } = await apiClient.get('/users', { signal });
return data;
},
});
}
export function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: async ({ signal }) => {
const { data } = await apiClient.get(`/users/${id}`, { signal });
return data;
},
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: NewUser) =>
apiClient.post('/users', newUser).then(res => res.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}API客户端:
tsx
// lib/api-client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 处理未授权
window.location.href = '/login';
}
return Promise.reject(error);
}
);查询Hook:
tsx
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async ({ signal }) => {
const { data } = await apiClient.get('/users', { signal });
return data;
},
});
}
export function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: async ({ signal }) => {
const { data } = await apiClient.get(`/users/${id}`, { signal });
return data;
},
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: NewUser) =>
apiClient.post('/users', newUser).then(res => res.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}GraphQL Integration
GraphQL集成
Apollo Client Alternative:
tsx
import { useQuery } from '@tanstack/react-query';
import { request, gql } from 'graphql-request';
const endpoint = 'https://api.example.com/graphql';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => request(endpoint, GET_USERS),
});
}
// With variables
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: async () => request(endpoint, GET_USER, { id }),
});
}Apollo Client替代方案:
tsx
import { useQuery } from '@tanstack/react-query';
import { request, gql } from 'graphql-request';
const endpoint = 'https://api.example.com/graphql';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => request(endpoint, GET_USERS),
});
}
// 带变量的查询
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: async () => request(endpoint, GET_USER, { id }),
});
}Zustand for Global State
Zustand全局状态
Combined Pattern:
tsx
// store/useAuthStore.ts
import { create } from 'zustand';
interface AuthState {
token: string | null;
setToken: (token: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
token: localStorage.getItem('token'),
setToken: (token) => {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
set({ token });
},
logout: () => {
localStorage.removeItem('token');
set({ token: null });
},
}));
// hooks/useAuthenticatedQuery.ts
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/store/useAuthStore';
export function useAuthenticatedQuery() {
const token = useAuthStore(state => state.token);
return useQuery({
queryKey: ['profile', token],
queryFn: async () => {
const response = await fetch('/api/profile', {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
},
enabled: !!token,
});
}组合模式:
tsx
// store/useAuthStore.ts
import { create } from 'zustand';
interface AuthState {
token: string | null;
setToken: (token: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
token: localStorage.getItem('token'),
setToken: (token) => {
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
set({ token });
},
logout: () => {
localStorage.removeItem('token');
set({ token: null });
},
}));
// hooks/useAuthenticatedQuery.ts
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/store/useAuthStore';
export function useAuthenticatedQuery() {
const token = useAuthStore(state => state.token);
return useQuery({
queryKey: ['profile', token],
queryFn: async () => {
const response = await fetch('/api/profile', {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
},
enabled: !!token,
});
}TypeScript Patterns
TypeScript模式
Typed Queries
类型化查询
Generic Query Hook:
tsx
interface User {
id: number;
name: string;
email: string;
}
// Explicit typing
const { data } = useQuery<User, Error>({
queryKey: ['user', id],
queryFn: async () => {
const response = await fetch(`/api/users/${id}`);
return response.json(); // TypeScript infers return type
},
});
// data is User | undefined
// error is Error | nullType-safe Query Keys:
tsx
// Define query keys with types
const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
};
// Usage with full type safety
const { data } = useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
});
// Invalidate with autocomplete
queryClient.invalidateQueries({ queryKey: userKeys.lists() });Custom Hook with Types:
tsx
interface User {
id: number;
name: string;
email: string;
}
interface UseUserOptions {
enabled?: boolean;
onSuccess?: (user: User) => void;
}
function useUser(id: number, options?: UseUserOptions) {
return useQuery({
queryKey: ['user', id],
queryFn: async (): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
enabled: options?.enabled,
// Type-safe callbacks
onSuccess: options?.onSuccess,
});
}
// Usage
const { data } = useUser(1, {
enabled: true,
onSuccess: (user) => {
console.log(user.name); // TypeScript knows user is User
},
});显式类型化:
tsx
interface User {
id: number;
name: string;
email: string;
}
// 显式类型
const { data } = useQuery<User, Error>({
queryKey: ['user', id],
queryFn: async () => {
const response = await fetch(`/api/users/${id}`);
return response.json(); // TypeScript自动推断返回类型
},
});
// data类型为 User | undefined
// error类型为 Error | null类型安全的查询键:
tsx
// 定义带类型的查询键
const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
};
// 带完整类型安全的使用
const { data } = useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
});
// 带自动补全的失效
queryClient.invalidateQueries({ queryKey: userKeys.lists() });带类型的自定义Hook:
tsx
interface User {
id: number;
name: string;
email: string;
}
interface UseUserOptions {
enabled?: boolean;
onSuccess?: (user: User) => void;
}
function useUser(id: number, options?: UseUserOptions) {
return useQuery({
queryKey: ['user', id],
queryFn: async (): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('获取用户失败');
return response.json();
},
enabled: options?.enabled,
// 类型安全的回调
onSuccess: options?.onSuccess,
});
}
// 使用
const { data } = useUser(1, {
enabled: true,
onSuccess: (user) => {
console.log(user.name); // TypeScript知道user是User类型
},
});Typed Mutations
类型化变更
tsx
interface CreateUserPayload {
name: string;
email: string;
}
interface User {
id: number;
name: string;
email: string;
}
function useCreateUser() {
return useMutation<User, Error, CreateUserPayload>({
mutationFn: async (payload) => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(payload),
});
return response.json();
},
onSuccess: (data) => {
// data is User
console.log('Created user:', data.name);
},
onError: (error) => {
// error is Error
console.error('Failed:', error.message);
},
});
}
// Usage
const mutation = useCreateUser();
mutation.mutate({ name: 'John', email: 'john@example.com' });tsx
interface CreateUserPayload {
name: string;
email: string;
}
interface User {
id: number;
name: string;
email: string;
}
function useCreateUser() {
return useMutation<User, Error, CreateUserPayload>({
mutationFn: async (payload) => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(payload),
});
return response.json();
},
onSuccess: (data) => {
// data类型为User
console.log('已创建用户:', data.name);
},
onError: (error) => {
// error类型为Error
console.error('创建失败:', error.message);
},
});
}
// 使用
const mutation = useCreateUser();
mutation.mutate({ name: 'John', email: 'john@example.com' });Query Client Typing
查询客户端类型化
tsx
import { QueryClient } from '@tanstack/react-query';
// Type-safe query client methods
const user = queryClient.getQueryData<User>(['user', 1]);
queryClient.setQueryData<User>(['user', 1], (old) => {
// old is User | undefined
if (!old) return old;
return { ...old, name: 'Updated' };
});
// Type-safe invalidation
queryClient.invalidateQueries<User>({
queryKey: ['users'],
predicate: (query) => {
// query.state.data is User | undefined
return query.state.data?.isActive === true;
},
});tsx
import { QueryClient } from '@tanstack/react-query';
// 类型安全的查询客户端方法
const user = queryClient.getQueryData<User>(['user', 1]);
queryClient.setQueryData<User>(['user', 1], (old) => {
// old类型为 User | undefined
if (!old) return old;
return { ...old, name: '已更新' };
});
// 类型安全的失效
queryClient.invalidateQueries<User>({
queryKey: ['users'],
predicate: (query) => {
// query.state.data类型为 User | undefined
return query.state.data?.isActive === true;
},
});Testing
测试
Setup Testing Environment
测试环境配置
Test Utils:
tsx
// test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { ReactNode } from 'react';
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // Don't retry failed queries in tests
gcTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {}, // Silence errors in tests
},
});
}
export function renderWithClient(ui: ReactNode) {
const testQueryClient = createTestQueryClient();
return render(
<QueryClientProvider client={testQueryClient}>
{ui}
</QueryClientProvider>
);
}测试工具:
tsx
// test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { ReactNode } from 'react';
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // 测试中不重试失败的查询
gcTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {}, // 测试中静默错误
},
});
}
export function renderWithClient(ui: ReactNode) {
const testQueryClient = createTestQueryClient();
return render(
<QueryClientProvider client={testQueryClient}>
{ui}
</QueryClientProvider>
);
}Testing Queries
测试查询
Basic Query Test:
tsx
// UserProfile.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';
const server = setupServer(
rest.get('/api/users/1', (req, res, ctx) => {
return res(
ctx.json({
id: 1,
name: 'John Doe',
email: 'john@example.com',
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('displays user profile', async () => {
renderWithClient(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
test('handles fetch error', async () => {
server.use(
rest.get('/api/users/1', (req, res, ctx) => {
return res(ctx.status(500));
})
);
renderWithClient(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});基础查询测试:
tsx
// UserProfile.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';
const server = setupServer(
rest.get('/api/users/1', (req, res, ctx) => {
return res(
ctx.json({
id: 1,
name: 'John Doe',
email: 'john@example.com',
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('显示用户资料', async () => {
renderWithClient(<UserProfile userId={1} />);
expect(screen.getByText('加载中...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
test('处理获取错误', async () => {
server.use(
rest.get('/api/users/1', (req, res, ctx) => {
return res(ctx.status(500));
})
);
renderWithClient(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText(/错误/i)).toBeInTheDocument();
});
});Testing Mutations
测试变更
tsx
// CreateUserForm.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CreateUserForm } from './CreateUserForm';
const server = setupServer(
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.json({
id: 1,
...body,
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('creates user successfully', async () => {
const user = userEvent.setup();
renderWithClient(<CreateUserForm />);
await user.type(screen.getByPlaceholderText('Name'), 'John Doe');
await user.type(screen.getByPlaceholderText('Email'), 'john@example.com');
await user.click(screen.getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(screen.getByText(/created successfully/i)).toBeInTheDocument();
});
});tsx
// CreateUserForm.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CreateUserForm } from './CreateUserForm';
const server = setupServer(
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.json({
id: 1,
...body,
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('成功创建用户', async () => {
const user = userEvent.setup();
renderWithClient(<CreateUserForm />);
await user.type(screen.getByPlaceholderText('姓名'), 'John Doe');
await user.type(screen.getByPlaceholderText('邮箱'), 'john@example.com');
await user.click(screen.getByRole('button', { name: /创建/i }));
await waitFor(() => {
expect(screen.getByText(/创建成功/i)).toBeInTheDocument();
});
});Testing with Mock Data
使用模拟数据测试
Hydrate Query Data:
tsx
test('renders with initial data', () => {
const testQueryClient = createTestQueryClient();
// Pre-populate cache
testQueryClient.setQueryData(['user', 1], {
id: 1,
name: 'John Doe',
email: 'john@example.com',
});
render(
<QueryClientProvider client={testQueryClient}>
<UserProfile userId={1} />
</QueryClientProvider>
);
// Data immediately available (no loading state)
expect(screen.getByText('John Doe')).toBeInTheDocument();
});水合查询数据:
tsx
test('使用初始数据渲染', () => {
const testQueryClient = createTestQueryClient();
// 预填充缓存
testQueryClient.setQueryData(['user', 1], {
id: 1,
name: 'John Doe',
email: 'john@example.com',
});
render(
<QueryClientProvider client={testQueryClient}>
<UserProfile userId={1} />
</QueryClientProvider>
);
// 数据立即可用(无加载状态)
expect(screen.getByText('John Doe')).toBeInTheDocument();
});Testing Custom Hooks
测试自定义Hook
tsx
// useUser.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { useUser } from './useUser';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'John Doe' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetches user data', async () => {
const queryClient = new QueryClient();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useUser(1), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({ id: 1, name: 'John Doe' });
});tsx
// useUser.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { useUser } from './useUser';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'John Doe' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('获取用户数据', async () => {
const queryClient = new QueryClient();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useUser(1), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({ id: 1, name: 'John Doe' });
});Performance Optimization
性能优化
Query Deduplication
查询去重
Automatic Deduplication:
tsx
// Multiple components request same data - only one network request
function Dashboard() {
return (
<div>
<UserStats userId={1} /> {/* Triggers fetch */}
<UserProfile userId={1} /> {/* Uses cache */}
<UserActivity userId={1} /> {/* Uses cache */}
</div>
);
}自动去重:
tsx
// 多个组件请求相同数据 - 仅发起一次网络请求
function Dashboard() {
return (
<div>
<UserStats userId={1} /> {/* 触发请求 */}
<UserProfile userId={1} /> {/* 使用缓存 */}
<UserActivity userId={1} /> {/* 使用缓存 */}
</div>
);
}Prefetching
预获取
Hover Prefetch:
tsx
function UserLink({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60000,
});
};
return (
<Link
href={`/users/${userId}`}
onMouseEnter={prefetchUser}
onFocus={prefetchUser}
>
View User
</Link>
);
}Route Prefetch:
tsx
// Next.js App Router
import { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query';
export default async function UserPage({ params }: { params: { id: string } }) {
const queryClient = new QueryClient();
// Prefetch user data
await queryClient.prefetchQuery({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
// Prefetch related data
await queryClient.prefetchQuery({
queryKey: ['user-posts', params.id],
queryFn: () => fetchUserPosts(params.id),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={params.id} />
</HydrationBoundary>
);
}悬停预获取:
tsx
function UserLink({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60000,
});
};
return (
<Link
href={`/users/${userId}`}
onMouseEnter={prefetchUser}
onFocus={prefetchUser}
>
查看用户
</Link>
);
}路由预获取:
tsx
// Next.js App Router
import { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query';
export default async function UserPage({ params }: { params: { id: string } }) {
const queryClient = new QueryClient();
// 预获取用户数据
await queryClient.prefetchQuery({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
// 预获取相关数据
await queryClient.prefetchQuery({
queryKey: ['user-posts', params.id],
queryFn: () => fetchUserPosts(params.id),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={params.id} />
</HydrationBoundary>
);
}Select and Transform Data
选择与转换数据
Memo-ized Selectors:
tsx
// Only re-render when selected data changes
function TodoList({ filter }: { filter: 'all' | 'done' | 'pending' }) {
const { data: filteredTodos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => {
// This only runs when todos change
if (filter === 'done') return todos.filter(t => t.done);
if (filter === 'pending') return todos.filter(t => !t.done);
return todos;
},
});
// Component only re-renders when filteredTodos change
return (
<ul>
{filteredTodos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}Expensive Computations:
tsx
const { data: sortedUsers } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
// Heavy sorting only runs when users change
return users
.slice()
.sort((a, b) => a.name.localeCompare(b.name));
},
});记忆化选择器:
tsx
// 仅当选中的数据变化时才重新渲染
function TodoList({ filter }: { filter: 'all' | 'done' | 'pending' }) {
const { data: filteredTodos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => {
// 仅当todos变化时才执行
if (filter === 'done') return todos.filter(t => t.done);
if (filter === 'pending') return todos.filter(t => !t.done);
return todos;
},
});
// 组件仅在filteredTodos变化时重新渲染
return (
<ul>
{filteredTodos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}昂贵计算:
tsx
const { data: sortedUsers } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
// 繁重的排序仅在users变化时执行
return users
.slice()
.sort((a, b) => a.name.localeCompare(b.name));
},
});Structural Sharing
结构共享
Automatic Structural Sharing:
tsx
// TanStack Query automatically does structural sharing
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
structuralSharing: true, // Default
});
// If refetch returns identical data structure,
// component doesn't re-render even though fetch completedCustom Structural Sharing:
tsx
import { replaceEqualDeep } from '@tanstack/react-query';
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
structuralSharing: (oldData, newData) => {
// Custom comparison logic
return replaceEqualDeep(oldData, newData);
},
});自动结构共享:
tsx
// TanStack Query自动执行结构共享
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
structuralSharing: true, // 默认开启
});
// 如果重新获取返回相同的数据结构,即使请求完成,组件也不会重新渲染自定义结构共享:
tsx
import { replaceEqualDeep } from '@tanstack/react-query';
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
structuralSharing: (oldData, newData) => {
// 自定义比较逻辑
return replaceEqualDeep(oldData, newData);
},
});Query Cancellation
查询取消
Abort In-Flight Requests:
tsx
const { data, refetch } = useQuery({
queryKey: ['search', searchTerm],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/search?q=${searchTerm}`, {
signal, // Pass abort signal
});
return response.json();
},
});
// When searchTerm changes, previous request is cancelled automaticallyManual Cancellation:
tsx
const queryClient = useQueryClient();
// Cancel all queries
queryClient.cancelQueries();
// Cancel specific query
queryClient.cancelQueries({ queryKey: ['todos'] });中止进行中的请求:
tsx
const { data, refetch } = useQuery({
queryKey: ['search', searchTerm],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/search?q=${searchTerm}`, {
signal, // 传递中止信号
});
return response.json();
},
});
// 当searchTerm变化时,之前的请求会自动取消手动取消:
tsx
const queryClient = useQueryClient();
// 取消所有查询
queryClient.cancelQueries();
// 取消特定查询
queryClient.cancelQueries({ queryKey: ['todos'] });Best Practices and Common Patterns
最佳实践与常见模式
Query Key Factories
查询键工厂
Centralized Query Keys:
tsx
// lib/query-keys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.users.details(), id] as const,
},
posts: {
all: ['posts'] as const,
lists: () => [...queryKeys.posts.all, 'list'] as const,
list: (filters: PostFilters) => [...queryKeys.posts.lists(), filters] as const,
details: () => [...queryKeys.posts.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
},
};
// Usage
const { data } = useQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => fetchUser(userId),
});
// Invalidate all user lists
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });集中式查询键:
tsx
// lib/query-keys.ts
export const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.users.details(), id] as const,
},
posts: {
all: ['posts'] as const,
lists: () => [...queryKeys.posts.all, 'list'] as const,
list: (filters: PostFilters) => [...queryKeys.posts.lists(), filters] as const,
details: () => [...queryKeys.posts.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
},
};
// 使用
const { data } = useQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => fetchUser(userId),
});
// 失效所有用户列表
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });Custom Hook Patterns
自定义Hook模式
Resource Hook Factory:
tsx
// lib/create-resource-hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function createResourceHooks<T, CreateT = Partial<T>, UpdateT = Partial<T>>(
resourceName: string,
api: {
getAll: () => Promise<T[]>;
getOne: (id: string | number) => Promise<T>;
create: (data: CreateT) => Promise<T>;
update: (id: string | number, data: UpdateT) => Promise<T>;
delete: (id: string | number) => Promise<void>;
}
) {
const keys = {
all: [resourceName] as const,
lists: () => [...keys.all, 'list'] as const,
details: () => [...keys.all, 'detail'] as const,
detail: (id: string | number) => [...keys.details(), id] as const,
};
return {
useList: () =>
useQuery({
queryKey: keys.lists(),
queryFn: api.getAll,
}),
useDetail: (id: string | number) =>
useQuery({
queryKey: keys.detail(id),
queryFn: () => api.getOne(id),
enabled: !!id,
}),
useCreate: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
useUpdate: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string | number; data: UpdateT }) =>
api.update(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: keys.detail(id) });
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
useDelete: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
};
}
// Usage
const userHooks = createResourceHooks('users', userApi);
function UsersList() {
const { data: users } = userHooks.useList();
const createUser = userHooks.useCreate();
const deleteUser = userHooks.useDelete();
return (
<div>
{users?.map(user => (
<div key={user.id}>
{user.name}
<button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
</div>
))}
</div>
);
}资源Hook工厂:
tsx
// lib/create-resource-hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function createResourceHooks<T, CreateT = Partial<T>, UpdateT = Partial<T>>(
resourceName: string,
api: {
getAll: () => Promise<T[]>;
getOne: (id: string | number) => Promise<T>;
create: (data: CreateT) => Promise<T>;
update: (id: string | number, data: UpdateT) => Promise<T>;
delete: (id: string | number) => Promise<void>;
}
) {
const keys = {
all: [resourceName] as const,
lists: () => [...keys.all, 'list'] as const,
details: () => [...keys.all, 'detail'] as const,
detail: (id: string | number) => [...keys.details(), id] as const,
};
return {
useList: () =>
useQuery({
queryKey: keys.lists(),
queryFn: api.getAll,
}),
useDetail: (id: string | number) =>
useQuery({
queryKey: keys.detail(id),
queryFn: () => api.getOne(id),
enabled: !!id,
}),
useCreate: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
useUpdate: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string | number; data: UpdateT }) =>
api.update(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: keys.detail(id) });
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
useDelete: () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: keys.lists() });
},
});
},
};
}
// 使用
const userHooks = createResourceHooks('users', userApi);
function UsersList() {
const { data: users } = userHooks.useList();
const createUser = userHooks.useCreate();
const deleteUser = userHooks.useDelete();
return (
<div>
{users?.map(user => (
<div key={user.id}>
{user.name}
<button onClick={() => deleteUser.mutate(user.id)}>删除</button>
</div>
))}
</div>
);
}Error Handling Patterns
错误处理模式
Centralized Error Handler:
tsx
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
if (error instanceof APIError) {
toast.error(`Error: ${error.message}`);
}
},
},
mutations: {
onError: (error) => {
toast.error(`Failed to save: ${error.message}`);
},
},
},
});集中式错误处理器:
tsx
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
if (error instanceof APIError) {
toast.error(`错误: ${error.message}`);
}
},
},
mutations: {
onError: (error) => {
toast.error(`保存失败: ${error.message}`);
},
},
},
});Migration from SWR
从SWR迁移
SWR to TanStack Query:
tsx
// Before (SWR)
import useSWR from 'swr';
function Profile() {
const { data, error, mutate } = useSWR('/api/user', fetcher);
if (error) return <div>Error</div>;
if (!data) return <div>Loading...</div>;
return <div>{data.name}</div>;
}
// After (TanStack Query)
import { useQuery, useQueryClient } from '@tanstack/react-query';
function Profile() {
const { data, error, isLoading } = useQuery({
queryKey: ['/api/user'],
queryFn: () => fetcher('/api/user'),
});
const queryClient = useQueryClient();
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['/api/user'] });
if (error) return <div>Error</div>;
if (isLoading) return <div>Loading...</div>;
return <div>{data.name}</div>;
}Comparison:
- →
useSWR(key, fetcher)useQuery({ queryKey: [key], queryFn: fetcher }) - →
mutate()queryClient.invalidateQueries() - loading →
!dataisLoading - →
useSWRConfig()useQueryClient()
SWR到TanStack Query:
tsx
// 之前(SWR)
import useSWR from 'swr';
function Profile() {
const { data, error, mutate } = useSWR('/api/user', fetcher);
if (error) return <div>错误</div>;
if (!data) return <div>加载中...</div>;
return <div>{data.name}</div>;
}
// 之后(TanStack Query)
import { useQuery, useQueryClient } from '@tanstack/react-query';
function Profile() {
const { data, error, isLoading } = useQuery({
queryKey: ['/api/user'],
queryFn: () => fetcher('/api/user'),
});
const queryClient = useQueryClient();
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['/api/user'] });
if (error) return <div>错误</div>;
if (isLoading) return <div>加载中...</div>;
return <div>{data.name}</div>;
}对比:
- →
useSWR(key, fetcher)useQuery({ queryKey: [key], queryFn: fetcher }) - →
mutate()queryClient.invalidateQueries() - 加载状态 →
!dataisLoading - →
useSWRConfig()useQueryClient()
DevTools
开发工具(DevTools)
Setup DevTools:
tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Production Build:
tsx
// DevTools are automatically excluded in production builds
// No need to conditionally renderDevTools Features:
- View all queries and their states
- Inspect query data and errors
- Manually trigger refetch
- Invalidate queries
- View query timelines
- Monitor cache size
- Debug network waterfalls
配置DevTools:
tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<你的应用 />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}生产构建:
tsx
// DevTools会在生产构建中自动排除
// 无需条件渲染DevTools功能:
- 查看所有查询及其状态
- 检查查询数据和错误
- 手动触发重新获取
- 失效查询
- 查看查询时间线
- 监控缓存大小
- 调试网络瀑布流
Common Pitfalls
常见陷阱
❌ Don't Create QueryClient Inside Component:
tsx
// WRONG - Creates new client on every render
function App() {
const queryClient = new QueryClient(); // ❌
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
// CORRECT - Stable client instance
function App() {
const [queryClient] = useState(() => new QueryClient()); // ✅
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}❌ Don't Use Query Data in Render Without Checking:
tsx
// WRONG - data might be undefined
function UserProfile() {
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
return <div>{data.name}</div>; // ❌ Crashes if data is undefined
}
// CORRECT - Handle loading state
function UserProfile() {
const { data, isLoading } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
if (isLoading) return <Spinner />; // ✅
return <div>{data.name}</div>;
}❌ Don't Forget Query Keys Are Dependencies:
tsx
// WRONG - Missing dependency in query key
function UserPosts({ userId, filter }: Props) {
const { data } = useQuery({
queryKey: ['posts'], // ❌ Missing userId and filter
queryFn: () => fetchUserPosts(userId, filter),
});
}
// CORRECT - All dependencies in key
function UserPosts({ userId, filter }: Props) {
const { data } = useQuery({
queryKey: ['posts', userId, filter], // ✅
queryFn: () => fetchUserPosts(userId, filter),
});
}❌ Don't Mutate Query Data Directly:
tsx
// WRONG - Mutating cached data
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
data.push(newTodo); // ❌ Mutates cache directly
// CORRECT - Use setQueryData
queryClient.setQueryData(['todos'], (old = []) => [...old, newTodo]); // ✅❌ 不要在组件内部创建QueryClient:
tsx
// 错误 - 每次渲染都会创建新的客户端
function App() {
const queryClient = new QueryClient(); // ❌
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
// 正确 - 稳定的客户端实例
function App() {
const [queryClient] = useState(() => new QueryClient()); // ✅
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}❌ 不要在渲染中直接使用查询数据而不检查:
tsx
// 错误 - data可能为undefined
function UserProfile() {
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
return <div>{data.name}</div>; // ❌ 数据为undefined时会崩溃
}
// 正确 - 处理加载状态
function UserProfile() {
const { data, isLoading } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
if (isLoading) return <Spinner />; // ✅
return <div>{data.name}</div>;
}❌ 不要忘记查询键是依赖项:
tsx
// 错误 - 查询键中缺少依赖
function UserPosts({ userId, filter }: Props) {
const { data } = useQuery({
queryKey: ['posts'], // ❌ 缺少userId和filter
queryFn: () => fetchUserPosts(userId, filter),
});
}
// 正确 - 所有依赖都在键中
function UserPosts({ userId, filter }: Props) {
const { data } = useQuery({
queryKey: ['posts', userId, filter], // ✅
queryFn: () => fetchUserPosts(userId, filter),
});
}❌ 不要直接修改查询数据:
tsx
// 错误 - 直接修改缓存数据
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
data.push(newTodo); // ❌ 直接修改缓存
// 正确 - 使用setQueryData
queryClient.setQueryData(['todos'], (old = []) => [...old, newTodo]); // ✅Summary
总结
TanStack Query is the industry-standard solution for server-state management in React applications. Use it for API data fetching, caching, synchronization, and real-time updates. It eliminates manual state management boilerplate and provides powerful features like automatic background refetching, optimistic updates, pagination, and intelligent cache management.
Key Takeaways:
- Use for fetching data with automatic caching
useQuery - Use for create/update/delete operations
useMutation - Query keys are the foundation of cache management
- Invalidate queries after mutations to keep UI in sync
- Leverage optimistic updates for instant UI feedback
- Use for pagination and infinite scroll
useInfiniteQuery - Combine with Zustand for client-state management
- Integrate seamlessly with tRPC, REST, and GraphQL
- Type everything with TypeScript for full type safety
- Test with MSW for realistic API mocking
Progressive Loading Pattern:
- Entry Point: Quick start and basic setup
- Intermediate: Queries, mutations, and cache management
- Advanced: Optimistic updates, SSR, integrations, and performance
For additional resources, visit the official documentation.
TanStack Query是React应用中服务端状态管理的行业标准解决方案。使用它进行API数据获取、缓存、同步和实时更新。它消除了手动状态管理的样板代码,并提供了强大的功能,如自动后台重新获取、乐观更新、分页和智能缓存管理。
核心要点:
- 使用进行带自动缓存的数据获取
useQuery - 使用进行创建/更新/删除操作
useMutation - 查询键是缓存管理的基础
- 变更后失效查询以保持UI同步
- 利用乐观更新实现即时UI反馈
- 使用实现分页和无限滚动
useInfiniteQuery - 与Zustand结合管理客户端状态
- 与tRPC、REST和GraphQL无缝集成
- 使用TypeScript实现完整的类型安全
- 使用MSW进行真实的API模拟测试
渐进式学习路径:
- 入门:快速开始与基础配置
- 中级:查询、变更与缓存管理
- 高级:乐观更新、SSR、集成与性能优化
更多资源,请访问官方文档。
Related Skills
相关技能
When using Tanstack Query, these skills enhance your workflow:
- react: React hooks and patterns for integrating TanStack Query
- nextjs: TanStack Query with Next.js App Router and Server Components
- zustand: Complementary client-state management (use together for hybrid state)
- test-driven-development: Testing queries, mutations, and cache behavior
[Full documentation available in these skills if deployed in your bundle]
使用Tanstack Query时,以下技能可提升你的工作流:
- react:React钩子与模式,用于集成TanStack Query
- nextjs:TanStack Query与Next.js App Router和服务端组件
- zustand:互补的客户端状态管理(结合使用实现混合状态)
- test-driven-development:测试查询、变更与缓存行为
[如果已部署在你的技能包中,可查看这些技能的完整文档]