tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack 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-query
bash
npm install @tanstack/react-query

DevTools (optional but recommended)

DevTools(可选但推荐)

npm install @tanstack/react-query-devtools
undefined
npm install @tanstack/react-query-devtools
undefined

Basic 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  cacheTime
States:
  • 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 inactive
Axios 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:
  • placeholderData
    : Not persisted to cache, purely UI
  • initialData
    : Persisted to cache as real data

占位符数据(即时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,
});
区别:
  • placeholderData
    :不持久化到缓存,仅用于UI展示
  • 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 | null
Type-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 completed
Custom 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 automatically
Manual 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()
  • !data
    loading →
    isLoading
  • 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()
  • !data
    加载状态 →
    isLoading
  • 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 render
DevTools 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
    useQuery
    for fetching data with automatic caching
  • Use
    useMutation
    for create/update/delete operations
  • 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
    useInfiniteQuery
    for pagination and infinite scroll
  • 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:测试查询、变更与缓存行为
[如果已部署在你的技能包中,可查看这些技能的完整文档]