tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Overview

概述

TanStack Query (formerly React Query) manages server state - data that lives on the server and needs to be fetched, cached, synchronized, and updated. It provides automatic caching, background refetching, stale-while-revalidate patterns, pagination, infinite scrolling, and optimistic updates out of the box.
Package:
@tanstack/react-query
Devtools:
@tanstack/react-query-devtools
Current Version: v5
TanStack Query(前身为React Query)用于管理服务端状态——即存储在服务端、需要被获取、缓存、同步和更新的数据。它开箱即用地提供了自动缓存、后台重新获取、stale-while-revalidate模式、分页、无限滚动和乐观更新等功能。
包:
@tanstack/react-query
开发者工具:
@tanstack/react-query-devtools
当前版本: v5

Installation

安装

bash
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools  # Optional
bash
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools  # 可选

Setup

配置

tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      gcTime: 1000 * 60 * 5, // 5 minutes (garbage collection)
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1分钟
      gcTime: 1000 * 60 * 5, // 5分钟(垃圾回收)
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

Core Concepts

核心概念

Query Keys

查询键(Query Keys)

Query keys uniquely identify cached data. They must be serializable arrays:
tsx
// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// With variables (dependency array pattern)
useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodos })

// Hierarchical keys for invalidation
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
useQuery({ queryKey: ['todos', todoId, 'comments'], queryFn: () => fetchComments(todoId) })

// Invalidation matches prefixes:
// queryClient.invalidateQueries({ queryKey: ['todos'] })
// ^ Invalidates ALL queries starting with 'todos'
查询键用于唯一标识缓存的数据,它们必须是可序列化的数组:
tsx
// 简单键
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// 带变量(依赖数组模式)
useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodos })

// 用于失效的层级键
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
useQuery({ queryKey: ['todos', todoId, 'comments'], queryFn: () => fetchComments(todoId) })

// 失效匹配前缀:
// queryClient.invalidateQueries({ queryKey: ['todos'] })
// ^ 会使所有以'todos'开头的查询失效

Query Functions

查询函数(Query Functions)

tsx
// Query function receives a QueryFunctionContext
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async ({ queryKey, signal, meta }) => {
    const [_key, id] = queryKey
    const response = await fetch(`/api/todos/${id}`, { signal })
    if (!response.ok) throw new Error('Failed to fetch')
    return response.json()
  },
})

// Using the signal for automatic cancellation
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const response = await fetch('/api/todos', { signal })
    return response.json()
  },
})
tsx
// 查询函数接收QueryFunctionContext参数
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async ({ queryKey, signal, meta }) => {
    const [_key, id] = queryKey
    const response = await fetch(`/api/todos/${id}`, { signal })
    if (!response.ok) throw new Error('获取失败')
    return response.json()
  },
})

// 使用signal实现自动取消
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const response = await fetch('/api/todos', { signal })
    return response.json()
  },
})

queryOptions Helper

queryOptions 工具函数

Create reusable, type-safe query configurations:
tsx
import { queryOptions } from '@tanstack/react-query'

export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoQueryOptions = (todoId: string) =>
  queryOptions({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodo(todoId),
    enabled: !!todoId,
  })

// Usage
const { data } = useQuery(todosQueryOptions)
const { data } = useSuspenseQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todosQueryOptions)
创建可复用、类型安全的查询配置:
tsx
import { queryOptions } from '@tanstack/react-query'

export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoQueryOptions = (todoId: string) =>
  queryOptions({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodo(todoId),
    enabled: !!todoId,
  })

// 使用示例
const { data } = useQuery(todosQueryOptions)
const { data } = useSuspenseQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todosQueryOptions)

Queries (useQuery)

查询(useQuery)

Basic Usage

基础用法

tsx
import { useQuery } from '@tanstack/react-query'

function Todos() {
  const {
    data,
    error,
    isLoading,      // First load, no data yet
    isFetching,     // Any fetch in progress (including background)
    isError,
    isSuccess,
    isPending,      // No data yet (same as isLoading in most cases)
    status,         // 'pending' | 'error' | 'success'
    fetchStatus,    // 'fetching' | 'paused' | 'idle'
    refetch,
    isStale,
    isPlaceholderData,
    dataUpdatedAt,
    errorUpdatedAt,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error.message} />
  return <TodoList todos={data} />
}
tsx
import { useQuery } from '@tanstack/react-query'

function Todos() {
  const {
    data,
    error,
    isLoading,      // 首次加载,尚无数据
    isFetching,     // 任何正在进行的获取操作(包括后台)
    isError,
    isSuccess,
    isPending,      // 尚无数据(大多数情况下与isLoading相同)
    status,         // 'pending' | 'error' | 'success'
    fetchStatus,    // 'fetching' | 'paused' | 'idle'
    refetch,
    isStale,
    isPlaceholderData,
    dataUpdatedAt,
    errorUpdatedAt,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error.message} />
  return <TodoList todos={data} />
}

Query Options

查询选项

tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,

  // Freshness
  staleTime: 5000,            // ms data stays fresh (default: 0)
  gcTime: 300000,             // ms unused data stays in cache (default: 5 min)

  // Refetching
  refetchInterval: 10000,     // Poll every 10s
  refetchIntervalInBackground: false, // Don't poll when tab hidden
  refetchOnMount: true,       // Refetch on component mount if stale
  refetchOnWindowFocus: true, // Refetch on window focus if stale
  refetchOnReconnect: true,   // Refetch on network reconnect

  // Retry
  retry: 3,                   // Number of retries (or function)
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),

  // Conditional
  enabled: !!userId,          // Only run when truthy

  // Initial/placeholder data
  initialData: () => cachedData,
  initialDataUpdatedAt: Date.now() - 10000,
  placeholderData: (previousData) => previousData, // keepPreviousData pattern
  placeholderData: initialTodos,

  // Transform
  select: (data) => data.filter(todo => !todo.done),

  // Structural sharing (default: true)
  structuralSharing: true,

  // Network mode
  networkMode: 'online', // 'online' | 'always' | 'offlineFirst'

  // Meta (accessible in query function context)
  meta: { purpose: 'user-facing' },
})
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,

  // 新鲜度
  staleTime: 5000,            // 数据保持新鲜的毫秒数(默认:0)
  gcTime: 300000,             // 未使用数据在缓存中保留的毫秒数(默认:5分钟)

  // 重新获取
  refetchInterval: 10000,     // 每10秒轮询一次
  refetchIntervalInBackground: false, // 标签页隐藏时不轮询
  refetchOnMount: true,       // 如果数据过期,组件挂载时重新获取
  refetchOnWindowFocus: true, // 如果数据过期,窗口获得焦点时重新获取
  refetchOnReconnect: true,   // 网络重连时重新获取

  // 重试
  retry: 3,                   // 重试次数(或函数)
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),

  // 条件触发
  enabled: !!userId,          // 仅当值为真时运行

  // 初始/占位数据
  initialData: () => cachedData,
  initialDataUpdatedAt: Date.now() - 10000,
  placeholderData: (previousData) => previousData, // keepPreviousData模式
  placeholderData: initialTodos,

  // 转换
  select: (data) => data.filter(todo => !todo.done),

  // 结构共享(默认:true)
  structuralSharing: true,

  // 网络模式
  networkMode: 'online', // 'online' | 'always' | 'offlineFirst'

  // 元数据(可在查询函数上下文中访问)
  meta: { purpose: 'user-facing' },
})

Mutations (useMutation)

变更(useMutation)

Basic Usage

基础用法

tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddTodo() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newTodo: { title: string }) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(res => res.json())
    },
    // Lifecycle callbacks
    onMutate: async (variables) => {
      // Called before mutationFn
      // Good for optimistic updates
      return { previousTodos } // context for onError
    },
    onSuccess: (data, variables, context) => {
      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
    onError: (error, variables, context) => {
      // Rollback optimistic updates
      queryClient.setQueryData(['todos'], context.previousTodos)
    },
    onSettled: (data, error, variables, context) => {
      // Always runs (success or error)
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Todo' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Adding...' : 'Add Todo'}
    </button>
  )
}
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddTodo() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newTodo: { title: string }) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(res => res.json())
    },
    // 生命周期回调
    onMutate: async (variables) => {
      // 在mutationFn之前调用
      // 适合做乐观更新
      return { previousTodos } // 供onError使用的上下文
    },
    onSuccess: (data, variables, context) => {
      // 使相关查询失效
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
    onError: (error, variables, context) => {
      // 回滚乐观更新
      queryClient.setQueryData(['todos'], context.previousTodos)
    },
    onSettled: (data, error, variables, context) => {
      // 无论成功或失败都会运行
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Todo' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? '添加中...' : '添加待办'}
    </button>
  )
}

Mutation State

变更状态

tsx
const {
  mutate,         // Fire-and-forget
  mutateAsync,    // Returns promise
  isPending,      // Mutation in progress
  isError,
  isSuccess,
  isIdle,         // Not yet fired
  data,           // Success response
  error,          // Error object
  reset,          // Reset state to idle
  variables,      // Variables passed to mutate
  status,         // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })
tsx
const {
  mutate,         // 触发后无需等待
  mutateAsync,    // 返回Promise
  isPending,      // 变更正在进行中
  isError,
  isSuccess,
  isIdle,         // 尚未触发
  data,           // 成功响应数据
  error,          // 错误对象
  reset,          // 将状态重置为idle
  variables,      // 传递给mutate的变量
  status,         // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })

Optimistic Updates

乐观更新

tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 1. Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 2. Snapshot previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 3. Optimistically update
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 4. Return context for rollback
    return { previousTodo }
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
  },
  onSettled: () => {
    // Always refetch to sync with server
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 1. 取消正在进行的重新获取
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 2. 快照之前的值
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 3. 执行乐观更新
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 4. 返回用于回滚的上下文
    return { previousTodo }
  },
  onError: (err, newTodo, context) => {
    // 发生错误时回滚
    queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
  },
  onSettled: () => {
    // 始终重新获取以与服务端同步
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

Optimistic Updates on Lists

列表的乐观更新

tsx
onMutate: async (newTodo) => {
  await queryClient.cancelQueries({ queryKey: ['todos'] })
  const previousTodos = queryClient.getQueryData(['todos'])

  queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

  return { previousTodos }
},
onError: (err, newTodo, context) => {
  queryClient.setQueryData(['todos'], context.previousTodos)
},
tsx
onMutate: async (newTodo) => {
  await queryClient.cancelQueries({ queryKey: ['todos'] })
  const previousTodos = queryClient.getQueryData(['todos'])

  queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

  return { previousTodos }
},
onError: (err, newTodo, context) => {
  queryClient.setQueryData(['todos'], context.previousTodos)
},

Query Invalidation

查询失效

tsx
const queryClient = useQueryClient()

// Invalidate all queries
queryClient.invalidateQueries()

// Invalidate by prefix
queryClient.invalidateQueries({ queryKey: ['todos'] })

// Invalidate exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true })

// Invalidate with predicate
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1]?.status === 'done',
})

// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'] })

// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] })

// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })
tsx
const queryClient = useQueryClient()

// 使所有查询失效
queryClient.invalidateQueries()

// 按前缀使查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] })

// 精确匹配使查询失效
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true })

// 按断言条件使查询失效
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1]?.status === 'done',
})

// 失效并立即重新获取
queryClient.refetchQueries({ queryKey: ['todos'] })

// 从缓存中移除
queryClient.removeQueries({ queryKey: ['todos', 1] })

// 重置为初始状态
queryClient.resetQueries({ queryKey: ['todos'] })

Infinite Queries

无限查询

tsx
import { useInfiniteQuery } from '@tanstack/react-query'

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    fetchPreviousPage,
    hasNextPage,
    hasPreviousPage,
    isFetchingNextPage,
    isFetchingPreviousPage,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: async ({ pageParam }) => {
      const res = await fetch(`/api/projects?cursor=${pageParam}`)
      return res.json()
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages, lastPageParam) => {
      return lastPage.nextCursor ?? undefined // undefined = no more pages
    },
    getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
      return firstPage.prevCursor ?? undefined
    },
    maxPages: 3, // Keep max 3 pages in cache (for performance)
  })

  return (
    <div>
      {data.pages.map((page) =>
        page.items.map((item) => <Item key={item.id} item={item} />)
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
      </button>
    </div>
  )
}
tsx
import { useInfiniteQuery } from '@tanstack/react-query'

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    fetchPreviousPage,
    hasNextPage,
    hasPreviousPage,
    isFetchingNextPage,
    isFetchingPreviousPage,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: async ({ pageParam }) => {
      const res = await fetch(`/api/projects?cursor=${pageParam}`)
      return res.json()
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages, lastPageParam) => {
      return lastPage.nextCursor ?? undefined // undefined表示没有更多页面
    },
    getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
      return firstPage.prevCursor ?? undefined
    },
    maxPages: 3, // 缓存中最多保留3页(提升性能)
  })

  return (
    <div>
      {data.pages.map((page) =>
        page.items.map((item) => <Item key={item.id} item={item} />)
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? '加载中...' : hasNextPage ? '加载更多' : '没有更多了'}
      </button>
    </div>
  )
}

Parallel Queries

并行查询

tsx
// Multiple independent queries run in parallel automatically
function Dashboard() {
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })

  // Both fetch simultaneously
}

// Dynamic parallel queries with useQueries
function UserProjects({ userIds }) {
  const queries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
    combine: (results) => ({
      data: results.map(r => r.data),
      pending: results.some(r => r.isPending),
    }),
  })
}
tsx
// 多个独立查询会自动并行运行
function Dashboard() {
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })

  // 两个查询同时获取数据
}

// 使用useQueries实现动态并行查询
function UserProjects({ userIds }) {
  const queries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
    combine: (results) => ({
      data: results.map(r => r.data),
      pending: results.some(r => r.isPending),
    }),
  })
}

Dependent Queries

依赖查询

tsx
// Sequential queries using enabled
function UserPosts({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPostsByUser(userId),
    enabled: !!userQuery.data, // Only run when user is loaded
  })
}
tsx
// 使用enabled实现顺序查询
function UserPosts({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPostsByUser(userId),
    enabled: !!userQuery.data, // 仅当用户数据加载完成后才运行
  })
}

Paginated Queries

分页查询

tsx
function PaginatedList() {
  const [page, setPage] = useState(1)

  const { data, isPlaceholderData } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
    placeholderData: (previousData) => previousData, // Keep showing old data
  })

  return (
    <div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
      {data.items.map(item => <Item key={item.id} item={item} />)}
      <button
        onClick={() => setPage(p => p + 1)}
        disabled={isPlaceholderData || !data.hasMore}
      >
        Next
      </button>
    </div>
  )
}
tsx
function PaginatedList() {
  const [page, setPage] = useState(1)

  const { data, isPlaceholderData } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
    placeholderData: (previousData) => previousData, // 保持显示旧数据
  })

  return (
    <div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
      {data.items.map(item => <Item key={item.id} item={item} />)}
      <button
        onClick={() => setPage(p => p + 1)}
        disabled={isPlaceholderData || !data.hasMore}
      >
        下一页
      </button>
    </div>
  )
}

Suspense Integration

Suspense 集成

tsx
import { useSuspenseQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'

// Component will suspend until data is loaded
function TodoList() {
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
  // data is guaranteed to be defined here
  return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}

// Wrap with Suspense boundary
function App() {
  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <TodoList />
      </Suspense>
    </ErrorBoundary>
  )
}

// Multiple suspense queries (fetch in parallel)
function Dashboard() {
  const [{ data: users }, { data: projects }] = useSuspenseQueries({
    queries: [
      { queryKey: ['users'], queryFn: fetchUsers },
      { queryKey: ['projects'], queryFn: fetchProjects },
    ],
  })
}
tsx
import { useSuspenseQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query'

// 组件会暂停直到数据加载完成
function TodoList() {
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
  // 此处data一定是已定义的
  return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}

// 用Suspense边界包裹
function App() {
  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <TodoList />
      </Suspense>
    </ErrorBoundary>
  )
}

// 多个Suspense查询(并行获取)
function Dashboard() {
  const [{ data: users }, { data: projects }] = useSuspenseQueries({
    queries: [
      { queryKey: ['users'], queryFn: fetchUsers },
      { queryKey: ['projects'], queryFn: fetchProjects },
    ],
  })
}

Prefetching

预获取

tsx
const queryClient = useQueryClient()

// Prefetch on hover
function TodoLink({ todoId }) {
  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['todo', todoId],
      queryFn: () => fetchTodo(todoId),
      staleTime: 5000, // Only prefetch if data older than 5s
    })
  }

  return (
    <Link to={`/todos/${todoId}`} onMouseEnter={prefetch}>
      Todo {todoId}
    </Link>
  )
}

// Prefetch in route loader (TanStack Router integration)
export const Route = createFileRoute('/todos/$todoId')({
  loader: ({ context: { queryClient }, params: { todoId } }) =>
    queryClient.ensureQueryData(todoQueryOptions(todoId)),
})

// Prefetch infinite queries
queryClient.prefetchInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  pages: 3, // Prefetch first 3 pages
})
tsx
const queryClient = useQueryClient()

// 鼠标悬停时预获取
function TodoLink({ todoId }) {
  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['todo', todoId],
      queryFn: () => fetchTodo(todoId),
      staleTime: 5000, // 仅当数据过期超过5秒时才预获取
    })
  }

  return (
    <Link to={`/todos/${todoId}`} onMouseEnter={prefetch}>
      待办 {todoId}
    </Link>
  )
}

// 在路由加载器中预获取(TanStack Router集成)
export const Route = createFileRoute('/todos/$todoId')({
  loader: ({ context: { queryClient }, params: { todoId } }) =>
    queryClient.ensureQueryData(todoQueryOptions(todoId)),
})

// 预获取无限查询
queryClient.prefetchInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  pages: 3, // 预获取前3页
})

SSR & Hydration

SSR 与 水合

Server-Side Prefetching

服务端预获取

tsx
// Server component or loader
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'

async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Page({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Todos />
    </HydrationBoundary>
  )
}
tsx
// 服务端组件或加载器
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'

async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Page({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Todos />
    </HydrationBoundary>
  )
}

Streaming SSR (React Server Components)

流式SSR(React服务端组件)

tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { makeQueryClient } from './query-client'

export default async function Page() {
  const queryClient = makeQueryClient()

  // Prefetch on server
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <TodoList />
    </HydrationBoundary>
  )
}
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { makeQueryClient } from './query-client'

export default async function Page() {
  const queryClient = makeQueryClient()

  // 在服务端预获取
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <TodoList />
    </HydrationBoundary>
  )
}

QueryClient API

QueryClient API

tsx
const queryClient = useQueryClient()

// Get cached data
queryClient.getQueryData(['todos'])

// Set cached data
queryClient.setQueryData(['todos'], updatedTodos)
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

// Get query state
queryClient.getQueryState(['todos'])

// Check if fetching
queryClient.isFetching({ queryKey: ['todos'] })
queryClient.isMutating()

// Cancel queries
queryClient.cancelQueries({ queryKey: ['todos'] })

// Invalidate (marks stale, refetches active)
queryClient.invalidateQueries({ queryKey: ['todos'] })

// Refetch (force refetch even if fresh)
queryClient.refetchQueries({ queryKey: ['todos'] })

// Remove from cache
queryClient.removeQueries({ queryKey: ['todos'] })

// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })

// Clear entire cache
queryClient.clear()

// Prefetch
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.ensureQueryData({ queryKey: ['todos'], queryFn: fetchTodos })

// Get/set defaults
queryClient.setQueryDefaults(['todos'], { staleTime: 10000 })
queryClient.getQueryDefaults(['todos'])
queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo })
tsx
const queryClient = useQueryClient()

// 获取缓存数据
queryClient.getQueryData(['todos'])

// 设置缓存数据
queryClient.setQueryData(['todos'], updatedTodos)
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

// 获取查询状态
queryClient.getQueryState(['todos'])

// 检查是否正在获取数据
queryClient.isFetching({ queryKey: ['todos'] })
queryClient.isMutating()

// 取消查询
queryClient.cancelQueries({ queryKey: ['todos'] })

// 失效(标记为过期,重新获取活跃查询)
queryClient.invalidateQueries({ queryKey: ['todos'] })

// 重新获取(强制重新获取即使数据是新鲜的)
queryClient.refetchQueries({ queryKey: ['todos'] })

// 从缓存中移除
queryClient.removeQueries({ queryKey: ['todos'] })

// 重置为初始状态
queryClient.resetQueries({ queryKey: ['todos'] })

// 清空整个缓存
queryClient.clear()

// 预获取
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.ensureQueryData({ queryKey: ['todos'], queryFn: fetchTodos })

// 获取/设置默认值
queryClient.setQueryDefaults(['todos'], { staleTime: 10000 })
queryClient.getQueryDefaults(['todos'])
queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo })

Testing

测试

tsx
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // Don't retry in tests
        gcTime: Infinity, // Prevent garbage collection during tests
      },
    },
  })
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test('fetches todos', async () => {
  const { result } = renderHook(() => useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  }), { wrapper: createWrapper() })

  await waitFor(() => expect(result.current.isSuccess).toBe(true))
  expect(result.current.data).toEqual(expectedTodos)
})

// Mock with setQueryData for component tests
test('renders todos', () => {
  const queryClient = new QueryClient()
  queryClient.setQueryData(['todos'], mockTodos)

  render(
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  )

  expect(screen.getByText('Todo 1')).toBeInTheDocument()
})
tsx
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // 测试中不重试
        gcTime: Infinity, // 测试期间防止垃圾回收
      },
    },
  })
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test('获取待办事项', async () => {
  const { result } = renderHook(() => useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  }), { wrapper: createWrapper() })

  await waitFor(() => expect(result.current.isSuccess).toBe(true))
  expect(result.current.data).toEqual(expectedTodos)
})

// 使用setQueryData进行组件测试
 test('渲染待办事项', () => {
  const queryClient = new QueryClient()
  queryClient.setQueryData(['todos'], mockTodos)

  render(
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  )

  expect(screen.getByText('Todo 1')).toBeInTheDocument()
})

TypeScript Patterns

TypeScript 模式

Typing Query Functions

为查询函数添加类型

tsx
interface Todo {
  id: number
  title: string
  completed: boolean
}

// Type is inferred from queryFn return type
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})
// data: Todo[] | undefined

// With select
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data): string[] => data.map(t => t.title),
})
// data: string[] | undefined
tsx
interface Todo {
  id: number
  title: string
  completed: boolean
}

// 类型从queryFn的返回值推断
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})
// data: Todo[] | undefined

// 使用select
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data): string[] => data.map(t => t.title),
})
// data: string[] | undefined

Typing Errors

为错误添加类型

tsx
// Default error type is Error
const { error } = useQuery<Todo[], AxiosError>({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

// Or register globally
declare module '@tanstack/react-query' {
  interface Register {
    defaultError: AxiosError
  }
}
tsx
// 默认错误类型是Error
const { error } = useQuery<Todo[], AxiosError>({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

// 或全局注册
declare module '@tanstack/react-query' {
  interface Register {
    defaultError: AxiosError
  }
}

Query Options Pattern (Recommended)

查询选项模式(推荐)

tsx
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'

export const todosOptions = queryOptions({
  queryKey: ['todos'] as const,
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoOptions = (id: string) =>
  queryOptions({
    queryKey: ['todos', id] as const,
    queryFn: () => fetchTodo(id),
    enabled: !!id,
  })

// Full type inference everywhere
const { data } = useQuery(todosOptions)
const { data } = useSuspenseQuery(todoOptions('123'))
await queryClient.ensureQueryData(todosOptions)
queryClient.invalidateQueries({ queryKey: todosOptions.queryKey })
tsx
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'

export const todosOptions = queryOptions({
  queryKey: ['todos'] as const,
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoOptions = (id: string) =>
  queryOptions({
    queryKey: ['todos', id] as const,
    queryFn: () => fetchTodo(id),
    enabled: !!id,
  })

// 所有地方都能获得完整的类型推断
const { data } = useQuery(todosOptions)
const { data } = useSuspenseQuery(todoOptions('123'))
await queryClient.ensureQueryData(todosOptions)
queryClient.invalidateQueries({ queryKey: todosOptions.queryKey })

Advanced Patterns

高级模式

Window Focus Refetching

窗口焦点重新获取

tsx
// Disable globally
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { refetchOnWindowFocus: false },
  },
})

// Custom focus manager
import { focusManager } from '@tanstack/react-query'

// For React Native
focusManager.setEventListener((handleFocus) => {
  const subscription = AppState.addEventListener('change', (state) => {
    handleFocus(state === 'active')
  })
  return () => subscription.remove()
})
tsx
// 全局禁用
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { refetchOnWindowFocus: false },
  },
})

// 自定义焦点管理器
import { focusManager } from '@tanstack/react-query'

// 适用于React Native
focusManager.setEventListener((handleFocus) => {
  const subscription = AppState.addEventListener('change', (state) => {
    handleFocus(state === 'active')
  })
  return () => subscription.remove()
})

Network Mode

网络模式

tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  // 'online' (default): only fetch when online
  // 'always': always fetch (useful for local-first)
  // 'offlineFirst': try fetch, use cache if offline
  networkMode: 'offlineFirst',
})
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  // 'online'(默认):仅在线时获取
  // 'always':始终获取(适合本地优先应用)
  // 'offlineFirst':尝试获取,离线时使用缓存
  networkMode: 'offlineFirst',
})

Query Cancellation

查询取消

tsx
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    // signal is AbortSignal - automatically cancelled on unmount or key change
    const res = await fetch('/api/todos', { signal })
    return res.json()
  },
})

// Manual cancellation
queryClient.cancelQueries({ queryKey: ['todos'] })
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    // signal是AbortSignal - 在组件卸载或键变更时自动取消
    const res = await fetch('/api/todos', { signal })
    return res.json()
  },
})

// 手动取消
queryClient.cancelQueries({ queryKey: ['todos'] })

Persistence

持久化

tsx
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24, // 24 hours
})
tsx
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24, // 24小时
})

Best Practices

最佳实践

  1. Use
    queryOptions
    helper
    for type-safe, reusable query configurations
  2. Structure query keys hierarchically for granular invalidation
  3. Set appropriate
    staleTime
    - 0 means always refetch on mount (default), increase for less dynamic data
  4. Use
    placeholderData
    (not
    initialData
    ) for keeping previous page data during pagination
  5. Prefer
    useSuspenseQuery
    when using Suspense boundaries for cleaner component code
  6. Use
    enabled
    for dependent queries, not conditional hook calls
  7. Always invalidate after mutations - don't rely solely on optimistic updates
  8. Cancel queries in
    onMutate
    before optimistic updates to prevent race conditions
  9. Use
    ensureQueryData
    in route loaders instead of
    prefetchQuery
    for immediate access
  10. Set
    retry: false
    in tests
    to avoid timeout issues
  11. Don't destructure the query result if you need to pass it around (breaks reactivity)
  12. Use
    select
    for derived data instead of transforming in the component
  13. Keep query functions pure - they should only fetch, not cause side effects
  14. Use
    gcTime: Infinity
    in tests to prevent cache cleanup during assertions
  1. 使用
    queryOptions
    工具函数
    创建类型安全、可复用的查询配置
  2. 层级化结构查询键以实现细粒度的失效控制
  3. 设置合适的
    staleTime
    - 0表示挂载时始终重新获取(默认值),对于动态性较低的数据可以增大该值
  4. 在分页时使用
    placeholderData
    (而非
    initialData
    )来保留上一页的数据
  5. **使用Suspense边界时优先选择
    useSuspenseQuery
    **以简化组件代码
  6. 使用
    enabled
    处理依赖查询
    ,而非条件式调用钩子
  7. 变更后始终使查询失效 - 不要仅依赖乐观更新
  8. onMutate
    中取消查询
    后再执行乐观更新以避免竞态条件
  9. **在路由加载器中使用
    ensureQueryData
    **而非
    prefetchQuery
    以立即获取数据
  10. **测试中设置
    retry: false
    **以避免超时问题
  11. 如果需要传递查询结果,不要解构它(会破坏响应式)
  12. 使用
    select
    处理派生数据
    而非在组件中转换
  13. 保持查询函数纯净 - 它们应该只负责获取数据,不要产生副作用
  14. **测试中设置
    gcTime: Infinity
    **以防止断言期间缓存被清理

Common Pitfalls

常见陷阱

  • Using
    initialData
    when you mean
    placeholderData
    (initialData counts as "fresh" data)
  • Not providing
    initialPageParam
    for infinite queries (required in v5)
  • Calling hooks conditionally (violates React rules)
  • Not cancelling queries before optimistic updates (race conditions)
  • Setting
    staleTime
    higher than
    gcTime
    (data gets garbage collected while "fresh")
  • Forgetting to wrap tests with
    QueryClientProvider
  • Using same
    QueryClient
    instance across tests (shared state)
  • Not awaiting
    invalidateQueries
    in mutation callbacks when order matters
  • 当你需要的是
    placeholderData
    时使用了
    initialData
    (initialData会被视为“新鲜”数据)
  • 无限查询未提供
    initialPageParam
    (v5中是必填项)
  • 条件式调用钩子(违反React规则)
  • 乐观更新前未取消查询(导致竞态条件)
  • staleTime
    设置得比
    gcTime
    大(数据在“新鲜”时被垃圾回收)
  • 测试时忘记用
    QueryClientProvider
    包裹
  • 测试间使用同一个
    QueryClient
    实例(共享状态)
  • 当顺序很重要时,在变更回调中未等待
    invalidateQueries
    完成