TanStackQueryv5服务器状态管理技能 tanstack-query

这个技能教授如何使用 TanStack Query v5(React Query)进行 React 应用中的服务器状态管理,包括数据获取、缓存、突变和错误处理。关键词:TanStack Query, React Query, useQuery, useMutation, 数据获取, 缓存, 服务器状态, 前端开发。

前端开发 0 次安装 0 次浏览 更新于 3/7/2026

名称: tanstack-query 描述: TanStack Query v5(React Query)服务器状态管理。用于数据获取、缓存、突变,或遇到 v4 迁移、陈旧数据、无效错误。

关键词: TanStack Query, React Query, useQuery, useMutation, useInfiniteQuery, useSuspenseQuery, QueryClient, QueryClientProvider, 数据获取, 服务器状态, 缓存, staleTime, gcTime, 查询无效, 预取, 乐观更新, 突变, 查询键, 查询函数, 错误边界, suspense, React Query DevTools, v5 迁移, v4 到 v5, 请求瀑布, 后台重新获取, cacheTime 重命名, loading 状态重命名, pending 状态, initialPageParam 必填, keepPreviousData 移除, placeholderData, 查询回调移除, onSuccess 移除, onError 移除, 对象语法必填 许可证: MIT

TanStack Query (React Query) v5

状态: 生产就绪 ✅ 最后更新: 2025-12-09 依赖: React 18.0+(推荐 18.3+), TypeScript 4.9+(首选 5.x) 最新版本: @tanstack/react-query@5.90.12, @tanstack/react-query-devtools@5.91.1, @tanstack/eslint-plugin-query@5.91.2


快速开始(5分钟)

1. 安装依赖

# 选择包管理器
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# 或
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# 或
bun add @tanstack/react-query@latest @tanstack/react-query-devtools@latest

为什么重要:

  • TanStack Query v5 需要 React 18+(使用 useSyncExternalStore)
  • DevTools 对于调试查询和突变至关重要
  • v5 有从 v4 的破坏性更改 - 使用最新版本以获取所有修复

2. 设置 QueryClient Provider

// src/main.tsx 或 src/index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'

// 创建客户端
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 分钟
      gcTime: 1000 * 60 * 60, // 1 小时(原名 cacheTime)
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </StrictMode>
)

关键:

  • QueryClientProvider 包裹整个应用
  • 配置 staleTime 以避免过度重新获取(默认为 0)
  • 使用 gcTime(不是 cacheTime - v5 中重命名)
  • DevTools 应在 provider 内

了解默认值(v5):

  • staleTime: 0 → 数据立即陈旧,因此在挂载/聚焦时重新获取,除非您提高它
  • gcTime: 5 * 60 * 1000 → 非活动数据在 5 分钟后被垃圾回收
  • retry: 3 在浏览器中,retry: 0 在服务器上
  • refetchOnWindowFocus: truerefetchOnReconnect: true
  • networkMode: 'online'(请求在离线时暂停)。切换到 'always' 用于 SSR/预取,不希望取消

3. 创建第一个查询

// src/hooks/useTodos.ts
import { useQuery } from '@tanstack/react-query'

type Todo = {
  id: number
  title: string
  completed: boolean
}

async function fetchTodos(): Promise<Todo[]> {
  const response = await fetch('/api/todos')
  if (!response.ok) {
    throw new Error('Failed to fetch todos')
  }
  return response.json()
}

export function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

// 在组件中使用:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()

  if (isPending) return <div>加载中...</div>
  if (isError) return <div>错误: {error.message}</div>

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

关键:

  • v5 需要对象语法: useQuery({ queryKey, queryFn })
  • 使用 isPending(不是 isLoading - 现在意思是“pending AND fetching”)
  • 总是在 queryFn 中抛出错误以进行适当的错误处理
  • QueryKey 应为数组以保持一致的缓存键

4. 创建第一个突变

// src/hooks/useAddTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

type NewTodo = {
  title: string
}

async function addTodo(newTodo: NewTodo) {
  const response = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  })
  if (!response.ok) throw new Error('Failed to add todo')
  return response.json()
}

export function useAddTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      // 使查询无效并重新获取 todos
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

// 在组件中使用:
function AddTodoForm() {
  const { mutate, isPending } = useAddTodo()

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    mutate({ title: formData.get('title') as string })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '添加中...' : '添加 Todo'}
      </button>
    </form>
  )
}

为什么有效:

  • 突变使用回调(onSuccess, onError, onSettled) - 查询不使用
  • invalidateQueries 触发后台重新获取
  • 突变默认不缓存(正确行为)

7步设置过程

步骤 1: 安装依赖

# 核心库(必需)
pnpm add @tanstack/react-query

# DevTools(强烈推荐用于开发)
pnpm add -D @tanstack/react-query-devtools

# 可选: ESLint 插件用于最佳实践
pnpm add -D @tanstack/eslint-plugin-query

包角色:

  • @tanstack/react-query - 核心 React hooks 和 QueryClient
  • @tanstack/react-query-devtools - 可视化调试器(仅开发,可摇树优化)
  • @tanstack/eslint-plugin-query - 捕获常见错误

版本要求:

  • React 18.0 或更高(使用 useSyncExternalStore
  • TypeScript 5.2+ 以获取最佳类型推断(可选但推荐)

步骤 2: 配置 QueryClient

// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 数据被视为新鲜的时间(在此期间不会重新获取)
      staleTime: 1000 * 60 * 5, // 5 分钟

      // 非活动数据在缓存中停留的时间,直到垃圾回收
      gcTime: 1000 * 60 * 60, // 1 小时(v5: 从 cacheTime 重命名)

      // 重试失败请求(默认为服务器上 0,客户端上 3)
      retry: (failureCount, error) => {
        if (error instanceof Response && error.status === 404) return false
        return failureCount < 3
      },

      // 窗口聚焦时重新获取(开发时可能烦人)
      refetchOnWindowFocus: false,

      // 网络重新连接时重新获取
      refetchOnReconnect: true,

      // 如果数据陈旧,组件挂载时重新获取
      refetchOnMount: true,
    },
    mutations: {
      // 失败时重试突变(通常不希望)
      retry: 0,
    },
  },
})

关键配置决策:

staleTime vs gcTime:

  • staleTime: 数据被认为“陈旧”并可能重新获取的时间
    • 0(默认): 数据立即陈旧,挂载/聚焦时重新获取
    • 1000 * 60 * 5: 数据新鲜 5 分钟,在此期间不重新获取
    • Infinity: 数据永远不陈旧,仅手动无效
  • gcTime: 未使用数据在缓存中停留的时间
    • 1000 * 60 * 5(默认): 5 分钟
    • Infinity: 永不垃圾回收(内存泄漏风险)

何时重新获取:

  • refetchOnWindowFocus: true - 适用于频繁变化的数据(股票价格)
  • refetchOnWindowFocus: false - 适用于稳定数据或开发期间
  • refetchOnMount: true - 确保组件挂载时有新鲜数据
  • refetchOnReconnect: true - 网络重新连接后重新获取

步骤 3: 用 Provider 包裹应用

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/query-client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools
        initialIsOpen={false}
        buttonPosition="bottom-right"
      />
    </QueryClientProvider>
  </StrictMode>
)

Provider 放置:

  • 必须包裹所有使用 TanStack Query hooks 的组件
  • DevTools 必须在 provider 内
  • 整个应用仅一个 QueryClient 实例

DevTools 配置:

  • initialIsOpen={false} - 默认折叠
  • buttonPosition="bottom-right" - 显示切换按钮的位置
  • 在生产构建中自动移除(可摇树优化)

高级设置(步骤 4-7)

详细模式: 实现自定义查询钩子、带乐观更新的突变、DevTools 配置或错误边界时,加载 references/advanced-setup.md

快速摘要:

步骤 4: 自定义查询钩子 - 使用 queryOptions 工厂用于可重用模式。创建封装 API 调用的自定义钩子。

步骤 5: 突变 - 使用 useMutationonSuccess 使查询无效。对于即时 UI 反馈,使用 onMutate/onError/onSettled 模式实现乐观更新。

步骤 6: DevTools - 已在步骤 3 中包含。参考中提供高级选项用于自定义。

步骤 7: 错误边界 - 使用带有 React 错误边界的 QueryErrorResetBoundary。配置 throwOnError 选项用于全局 vs 局部错误处理。


关键规则

总是做

对所有钩子使用对象语法

// v5 仅支持此:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })

使用数组查询键

queryKey: ['todos']              // 列表
queryKey: ['todos', id]          // 详情
queryKey: ['todos', { filter }]  // 过滤

适当配置 staleTime

staleTime: 1000 * 60 * 5 // 5 分钟 - 防止过度重新获取

使用 isPending 用于初始加载状态

if (isPending) return <Loading />
// isPending = 尚无数据 AND 获取中

在 queryFn 中抛出错误

if (!response.ok) throw new Error('失败')

突变后使查询无效

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}

使用 queryOptions 工厂用于可重用模式

const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)

使用 gcTime(非 cacheTime)

gcTime: 1000 * 60 * 60 // 1 小时

了解状态标志

isPending      // 尚无数据,获取进行中
isFetching     // 任何获取进行中(包括重新获取)
isRefetching   // 专门重新获取(数据已缓存)
isLoadingError // 初始加载失败
isPaused       // networkMode 暂停(例如,离线)
isFetchingNextPage // useInfiniteQuery 加载更多

绝不做

绝不使用 v4 数组/函数语法

// v4(v5 中移除):
useQuery(['todos'], fetchTodos, options) // ❌

// v5(正确):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅

绝不使用查询回调(onSuccess, onError, onSettled 在查询中)

// v5 从查询中移除了这些:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ v5 中移除
})

// 改用 useEffect:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // 做某事
  }
}, [data])

// 或使用突变回调(仍支持):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ 仍适用于突变
})

绝不使用已弃用选项

// v5 中弃用:
cacheTime: 1000 // ❌ 改用 gcTime
isLoading: true // ❌ 含义更改,使用 isPending
keepPreviousData: true // ❌ 改用 placeholderData
onSuccess: () => {} // ❌ 从查询中移除
useErrorBoundary: true // ❌ 改用 throwOnError

绝不假设 isLoading 意为“尚无数据”

// v5 更改了此:
isLoading = isPending && isFetching // ❌ 现在意为“pending AND fetching”
isPending = 尚无数据 // ✅ 用于初始加载

绝不忘记 infinite queries 的 initialPageParam

// v5 需要此:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ v5 中必需
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

绝不将 enabled 与 useSuspenseQuery 使用

// 不允许:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ 不适用于 suspense
})

// 改用条件渲染:
{id && <TodoComponent id={id} />}

错误预防

此技能防止 8+ 记录的 v5 迁移问题。最关键错误包括:

  • 对象语法必需(v4 函数重载移除)
  • 查询回调移除(onSuccess/onError/onSettled)
  • isPending vs isLoading 状态更改
  • cacheTime 重命名为 gcTime
  • initialPageParam 必需用于 infinite queries
  • keepPreviousData 替换为 placeholderData

完整错误目录及前后示例: 遇到错误或调试 v5 迁移问题时,加载 references/top-errors.md


项目配置

基本配置文件: package.json, tsconfig.json, .eslintrc.cjs

关键要求:

  • React 18.3.1+(使用 useSyncExternalStore)
  • 推荐 TypeScript 严格模式
  • ESLint 插件捕获 v4→v5 迁移错误

完整配置模板: 设置新项目或排除构建/类型错误时,加载 references/configuration-files.md


常见模式

模式 1: 依赖查询

// 先获取用户,然后获取用户的帖子
function UserPosts({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!user, // 仅用户加载后获取帖子
  })

  if (!user) return <div>加载用户中...</div>
  if (!posts) return <div>加载帖子中...</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

何时使用: 查询 B 依赖查询 A 的数据

模式 2: 使用 useQueries 并行查询

// 并行获取多个 todos
function TodoDetails({ ids }: { ids: number[] }) {
  const results = useQueries({
    queries: ids.map(id => ({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
    })),
  })

  const isLoading = results.some(result => result.isPending)
  const isError = results.some(result => result.isError)

  if (isLoading) return <div>加载中...</div>
  if (isError) return <div>错误加载 todos</div>

  return (
    <ul>
      {results.map((result, i) => (
        <li key={ids[i]}>{result.data?.title}</li>
      ))}
    </ul>
  )
}

何时使用: 并行获取多个独立查询

模式 3: 预取

import { useQueryClient } from '@tanstack/react-query'
import { todosQueryOptions } from './hooks/useTodos'

function TodoListWithPrefetch() {
  const queryClient = useQueryClient()
  const { data: todos } = useTodos()

  const prefetchTodo = (id: number) => {
    queryClient.prefetchQuery({
      queryKey: ['todos', id],
      queryFn: () => fetchTodo(id),
      staleTime: 1000 * 60 * 5, // 5 分钟
    })
  }

  return (
    <ul>
      {todos?.map(todo => (
        <li
          key={todo.id}
          onMouseEnter={() => prefetchTodo(todo.id)}
        >
          <Link to={`/todos/${todo.id}`}>{todo.title}</Link>
        </li>
      ))}
    </ul>
  )
}

何时使用: 用户导航前预加载数据(悬停、挂载)

模式 4: 无限滚动

import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'

type Page = {
  data: Todo[]
  nextCursor: number | null
}

async function fetchTodosPage({ pageParam }: { pageParam: number }): Promise<Page> {
  const response = await fetch(`/api/todos?cursor=${pageParam}&limit=20`)
  return response.json()
}

function InfiniteTodoList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['todos', 'infinite'],
    queryFn: fetchTodosPage,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  })

  const loadMoreRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage()
        }
      },
      { threshold: 0.1 }
    )

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current)
    }

    return () => observer.disconnect()
  }, [fetchNextPage, hasNextPage])

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.data.map(todo => (
            <div key={todo.id}>{todo.title}</div>
          ))}
        </div>
      ))}

      <div ref={loadMoreRef}>
        {isFetchingNextPage && <div>加载更多...</div>}
      </div>
    </div>
  )
}

何时使用: 分页列表带无限滚动

模式 5: 查询取消

function SearchTodos() {
  const [search, setSearch] = useState('')

  const { data } = useQuery({
    queryKey: ['todos', 'search', search],
    queryFn: async ({ signal }) => {
      const response = await fetch(`/api/todos?q=${search}`, { signal })
      return response.json()
    },
    enabled: search.length > 2, // 仅搜索 3+ 字符
  })

  return (
    <div>
      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="搜索 todos..."
      />
      {data && (
        <ul>
          {data.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

如何工作:

  • 当 queryKey 更改时,先前查询自动取消
  • 传递 signal 给 fetch 以进行适当清理
  • 浏览器中止待处理获取请求

模式 6: 后台获取指示器

const { data, isFetching, isRefetching } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60 * 5,
})

return (
  <div>
    {isFetching && <Spinner label={isRefetching ? '刷新中…' : '加载中…'} />}
    <TodoList data={data} />
  </div>
)

为什么: isFetching 在后台重新获取期间保持 true,因此您可以显示细微的“刷新中”徽章而不丢失缓存数据。


使用捆绑资源

模板 (templates/)

完整、可复制的代码示例:

  • package.json - 带确切版本的依赖
  • query-client-config.ts - 带最佳实践的 QueryClient 设置
  • provider-setup.tsx - 带 QueryClientProvider 的应用包装器
  • use-query-basic.tsx - 基本 useQuery 钩子模式
  • use-mutation-basic.tsx - 基本 useMutation 钩子
  • use-mutation-optimistic.tsx - 乐观更新模式
  • use-infinite-query.tsx - 无限滚动模式
  • custom-hooks-pattern.tsx - 可重用查询钩子带 queryOptions
  • error-boundary.tsx - 带查询重置的错误边界
  • devtools-setup.tsx - DevTools 配置

示例使用:

# 复制查询客户端配置
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/

# 复制 provider 设置
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx

# 或运行引导助手(安装依赖 + 复制核心文件):
./scripts/example-script.sh . pnpm

参考 (references/)

深度文档,需要时加载:

  • advanced-setup.md - 自定义钩子、突变、乐观更新、DevTools、错误边界
  • configuration-files.md - 完整 package.json, tsconfig.json, .eslintrc.cjs 模板
  • v4-to-v5-migration.md - 完整 v4 → v5 迁移指南
  • best-practices.md - 请求瀑布、缓存策略、性能
  • common-patterns.md - 可重用查询、乐观更新、无限滚动
  • official-guides-map.md - 何时打开每个官方文档及其覆盖内容
  • typescript-patterns.md - 类型安全、泛型、类型推断
  • testing.md - 使用 MSW、React Testing Library 测试
  • top-errors.md - 所有 8+ 错误带解决方案

示例 (examples/)

  • examples/README.md - 前 10 场景索引带官方链接
  • basic.tsx - 最小列表查询
  • basic-graphql-request.tsx - GraphQL 客户端 + select
  • optimistic-update.tsx - onMutate 快照/回滚
  • pagination.tsx - 分页列表带 placeholderData
  • infinite-scroll.tsx - useInfiniteQuery + IntersectionObserver
  • prefetching.tsx - 导航前悬停预取
  • suspense.tsx - useSuspenseQuery + 边界
  • default-query-function.ts - 使用 queryKey 的全局 fetcher
  • nextjs-app-router.tsx - App Router 预取 + 水合(networkMode: 'always'
  • react-native.tsx - 离线优先带 AsyncStorage 持久器

Claude 应何时加载这些:

  • advanced-setup.md - 实现自定义查询钩子、突变或错误边界时
  • configuration-files.md - 设置新项目或排除构建/类型错误时
  • v4-to-v5-migration.md - 迁移现有 React Query v4 项目时
  • best-practices.md - 优化性能或避免瀑布时
  • common-patterns.md - 实现特定功能(无限滚动等)时
  • typescript-patterns.md - 处理 TypeScript 错误或类型推断时
  • testing.md - 为使用 TanStack Query 的组件编写测试时
  • top-errors.md - 遇到主 SKILL.md 未涵盖的错误时

高级主题

使用 select 进行数据转换

// 仅订阅数据的特定切片
function TodoCount() {
  const { data: count } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.length, // 仅计数更改时重新渲染
  })

  return <div>总计 todos: {count}</div>
}

// 转换数据形状
function CompletedTodoTitles() {
  const { data: titles } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) =>
      data
        .filter(todo => todo.completed)
        .map(todo => todo.title),
  })

  return (
    <ul>
      {titles?.map((title, i) => (
        <li key={i}>{title}</li>
      ))}
    </ul>
  )
}

好处:

  • 组件仅在选择的数据更改时重新渲染
  • 减少内存使用(组件状态中存储更少数据)
  • 保持查询缓存不变(其他组件获取完整数据)

请求瀑布(反模式)

// ❌ 差: 顺序瀑布
function BadUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: posts } = useQuery({
    queryKey: ['posts', user?.id],
    queryFn: () => fetchPosts(user!.id),
    enabled: !!user,
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', posts?.[0]?.id],
    queryFn: () => fetchComments(posts![0].id),
    enabled: !!posts && posts.length > 0,
  })

  // 每个查询等待前一个 = 慢!
}

// ✅ 好: 可能时并行获取
function GoodUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  // 并行获取帖子和评论
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId), // 不等用户
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', userId],
    queryFn: () => fetchUserComments(userId), // 不等帖子
  })

  // 所有 3 查询并行运行 = 快!
}

服务器状态 vs 客户端状态

// ❌ 不将 TanStack Query 用于仅客户端状态
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)

// ✅ 使用 useState 用于客户端状态
const [isModalOpen, setIsModalOpen] = useState(false)

// ✅ 使用 TanStack Query 用于服务器状态
const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

经验法则:

  • 服务器状态: 使用 TanStack Query(来自 API 的数据)
  • 客户端状态: 使用 useState/useReducer(本地 UI 状态)
  • 全局客户端状态: 使用 Zustand/Context(主题、身份验证令牌)

平台和集成说明

  • React Native: 与 web 相同工作。使用 @tanstack/query-async-storage-persister 持久缓存到 AsyncStorage;避免窗口聚焦重新获取逻辑。DevTools 面板本地不可用 — 使用 Flipper 或暴露日志。
  • GraphQL: 与 graphql-request 或 urql 的裸客户端配对。将操作视为普通异步函数;共置片段并使用 select 将 edges/nodes 映射到扁平形状。
  • SSR / Next.js / TanStack Start: 在服务器上使用 dehydrate/HydrationBoundary 和在客户端上使用 QueryClientProvider。为服务器预取设置 networkMode: 'always' 以便请求永不暂停。
  • Suspense: 优先 useSuspenseQuery 用于已使用 Suspense 的路由。不与 enabled 结合;改用条件渲染。
  • 测试: 使用 @testing-library/react + @tanstack/react-query/testing 助手和使用 MSW 模拟网络。测试间重置 QueryClient 以避免缓存污染。

依赖

必需:

  • @tanstack/react-query@5.90.12 - 核心库
  • react@18.0.0+ - 使用 useSyncExternalStore 钩子
  • react-dom@18.0.0+ - React DOM 渲染器

推荐:

  • @tanstack/react-query-devtools@5.91.1 - 可视化调试器(仅开发)
  • @tanstack/eslint-plugin-query@5.91.2 - 最佳实践 ESLint 规则
  • typescript@5.2.0+ - 用于类型安全和推断

可选:

  • @tanstack/query-sync-storage-persister - 持久缓存到 localStorage
  • @tanstack/query-async-storage-persister - 持久到 AsyncStorage(React Native)

官方文档


包版本(验证 2025-12-09)

{
  "dependencies": {
    "@tanstack/react-query": "^5.90.12"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.91.1",
    "@tanstack/eslint-plugin-query": "^5.91.2"
  }
}

验证:

  • npm view @tanstack/react-query version → 5.90.12
  • npm view @tanstack/react-query-devtools version → 5.91.1
  • npm view @tanstack/eslint-plugin-query version → 5.91.2
  • 最后检查: 2025-12-09

生产示例

此技能基于生产中使用的模式:

  • 构建时间: ~6 小时研究 + 开发
  • 防止的错误: 8(所有记录的 v5 迁移问题)
  • 令牌效率: ~65% 节省 vs 手动设置
  • 验证: ✅ 所有模式使用 TypeScript 严格模式测试

故障排除

问题: “useQuery is not a function” 或类型错误

解决方案: 确保使用 v5 对象语法:

// ✅ 正确:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// ❌ 错误(v4 语法):
useQuery(['todos'], fetchTodos)

问题: 回调(onSuccess, onError)在查询中不工作

解决方案: v5 中移除。使用 useEffect 或移到突变:

// ✅ 用于查询:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // 处理成功
  }
}, [data])

// ✅ 用于突变(仍工作):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => { /* ... */ },
})

问题: isLoading 始终 false,即使初始加载期间

解决方案: 改用 isPending:

const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = 尚无数据
// isLoading = isPending && isFetching
// isFetching = 任何获取进行中

问题: cacheTime 选项不识别

解决方案: v5 中重命名为 gcTime:

gcTime: 1000 * 60 * 60 // 1 小时

问题: useSuspenseQuery 带 enabled 选项给出类型错误

解决方案: enabled 不适用于 suspense。使用条件渲染:

{id && <TodoComponent id={id} />}

问题: 突变后数据不重新获取

解决方案: 在 onSuccess 中使查询无效:

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}

问题: Infinite query 需要 initialPageParam

解决方案: v5 中始终提供 initialPageParam:

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // 必需
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

问题: keepPreviousData 不工作

解决方案: 替换为 placeholderData:

import { keepPreviousData } from '@tanstack/react-query'

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})

完整设置清单

使用此清单验证设置:

  • [ ] 安装 @tanstack/react-query@5.90.12+
  • [ ] 安装 @tanstack/react-query-devtools(开发依赖)
  • [ ] 创建带配置默认值的 QueryClient
  • [ ] 用 QueryClientProvider 包裹应用
  • [ ] 添加 ReactQueryDevtools 组件
  • [ ] 创建第一个查询使用对象语法
  • [ ] 测试 isPending 和错误状态
  • [ ] 创建第一个突变带 onSuccess 处理器
  • [ ] 设置突变后查询无效
  • [ ] 适当配置 staleTime 和 gcTime
  • [ ] 一致使用数组 queryKey
  • [ ] 在 queryFn 中抛出错误
  • [ ] 无 v4 语法(函数重载)
  • [ ] 无查询回调(onSuccess, onError 在查询中)
  • [ ] 使用 isPending(非 isLoading)用于初始加载
  • [ ] DevTools 在开发中工作
  • [ ] TypeScript 类型正确工作
  • [ ] 生产构建成功

问题?问题?

  1. 检查 references/top-errors.md 获取完整错误解决方案
  2. 验证设置过程的所有步骤
  3. 检查官方文档: https://tanstack.com/query/latest
  4. 确保使用 v5 语法(对象语法, gcTime, isPending)
  5. 加入 Discord: https://tlinz.com/discord