mcpField
T
tanstack-query

tanstack-query

github.com/jackspace开发1652 次安装13 周前更新
代码仓库

关于 tanstack-query

tanstack-query 是用于在 React 应用中使用 TanStack Query v5 的技能,涵盖配置、数据获取、缓存、变更和调试,并说明如何避免迁移常见问题。

主要功能

  1. 01配置 QueryClient 和 Provider
  2. 02构建查询与变更
  3. 03管理缓存和失效
  4. 04使用乐观更新
  5. 05通过 React Query DevTools 调试
  6. 06规避 v5 常见迁移问题

适用场景

  1. 01为 React 应用添加服务端状态获取
  2. 02实现带缓存更新的变更流程
  3. 03在开发中调试查询
  4. 04将代码迁移至 TanStack Query v5 用法

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jackspace/claudeskillz --skill tanstack-query

安装后在 Claude Code 中输入/tanstack-query确认已加载。

附带文件README.mdSKILL.json

详细介绍

TanStack Query(React Query)v5

状态:已可用于生产环境 ✅ 最后更新:2025-10-22 依赖项:React 18.0+、TypeScript 4.7+(推荐) 最新版本:@tanstack/[email protected]、@tanstack/[email protected]


快速开始(5 分钟)

1. 安装依赖项
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest

为何重要:

  • TanStack Query v5 要求 React 18+(使用 useSyncExternalStore)
  • DevTools 对调试查询和变更至关重要
  • v5 有相对于 v4 的破坏性变更——请使用最新版本以获取所有修复
2. 设置 QueryClient Provider
// src/main.tsx or 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'

// Create a client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 60, // 1 hour (formerly 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 内部
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,
  })
}

// Usage in component:
function TodoList() {
  const { data, isPending, isError, error } = useTodos()

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error: {error.message}</div>

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

重要:

  • v5 要求使用对象语法:useQuery({ queryKey, queryFn })
  • 使用 isPending(而非 isLoading——现在它表示“待处理且正在获取”)
  • 始终在 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: () => {
      // Invalidate and refetch todos
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

// Usage in component:
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 ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  )
}

为何有效:

  • 变更使用回调(onSuccess、onError、onSettled),查询则不使用
  • invalidateQueries 会触发后台重新获取
  • 默认情况下,变更不会缓存(这是正确行为)

7 步设置流程

步骤 1:安装依赖项
# Core library (required)
npm install @tanstack/react-query

# DevTools (highly recommended for development)
npm install -D @tanstack/react-query-devtools

# Optional: ESLint plugin for best practices
npm install -D @tanstack/eslint-plugin-query

软件包的作用:

  • @tanstack/react-query - 核心 React hooks 和 QueryClient
  • @tanstack/react-query-devtools - 可视化调试工具(仅开发环境使用,可通过 tree-shaking 移除)
  • @tanstack/eslint-plugin-query - 检查常见错误

版本要求:

  • React 18.0 或更高版本(使用 useSyncExternalStore)
  • TypeScript 4.7+ 可获得最佳类型推断效果(可选,但推荐)
步骤 2:配置 QueryClient
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // How long data is considered fresh (won't refetch during this time)
      staleTime: 1000 * 60 * 5, // 5 minutes

      // How long inactive data stays in cache before garbage collection
      gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)

      // Retry failed requests (0 on server, 3 on client by default)
      retry: (failureCount, error) => {
        if (error instanceof Response && error.status === 404) return false
        return failureCount < 3
      },

      // Refetch on window focus (can be annoying during dev)
      refetchOnWindowFocus: false,

      // Refetch on network reconnect
      refetchOnReconnect: true,

      // Refetch on component mount if data is stale
      refetchOnMount: true,
    },
    mutations: {
      // Retry mutations on failure (usually don't want this)
      retry: 0,
    },
  },
})

关键配置决策:

staleTime 与 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" - 切换按钮的显示位置
  • 生产构建时会自动移除(通过 tree-shaking)
步骤 4:创建自定义查询 hooks

模式:可复用的查询 hooks

// src/api/todos.ts - API functions
export type Todo = {
  id: number
  title: string
  completed: boolean
}

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

export async function fetchTodoById(id: number): Promise<Todo> {
  const response = await fetch(`/api/todos/${id}`)
  if (!response.ok) {
    throw new Error(`Failed to fetch todo ${id}: ${response.statusText}`)
  }
  return response.json()
}

// src/hooks/useTodos.ts - Query hooks
import { useQuery, queryOptions } from '@tanstack/react-query'
import { fetchTodos, fetchTodoById } from '../api/todos'

// Query options factory (v5 pattern for reusability)
export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60, // 1 minute
})

export function useTodos() {
  return useQuery(todosQueryOptions)
}

export function useTodo(id: number) {
  return useQuery({
    queryKey: ['todos', id],
    queryFn: () => fetchTodoById(id),
    enabled: !!id, // Only fetch if id is truthy
  })
}

为何使用 queryOptions 工厂:

  • 类型推断效果完善
  • 可在 useQuery、useSuspenseQuery、prefetchQuery 中重复使用
  • 在所有地方保持 queryKey 和 queryFn 一致
  • 更易于测试和维护

查询键结构:

  • ['todos'] - 所有 todos 的列表
  • ['todos', id] - 单个 todo 的详情
  • ['todos', 'filters', { status: 'completed' }] - 筛选后的列表
  • 更具体的键是其子集(使 ['todos'] 失效会使所有相关查询失效)
步骤 5:通过乐观更新实现变更
// src/hooks/useTodoMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Todo } from '../api/todos'

type AddTodoInput = {
  title: string
}

type UpdateTodoInput = {
  id: number
  completed: boolean
}

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

  return useMutation({
    mutationFn: async (newTodo: AddTodoInput) => {
      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()
    },

    // Optimistic update
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] })

      // Snapshot previous value
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])

      // Optimistically update
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
        ...old,
        { id: Date.now(), ...newTodo, completed: false },
      ])

      // Return context with snapshot
      return { previousTodos }
    },

    // Rollback on error
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos)
    },

    // Always refetch after error or success
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  return useMutation({
    mutationFn: async ({ id, completed }: UpdateTodoInput) => {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed }),
      })
      if (!response.ok) throw new Error('Failed to update todo')
      return response.json()
    },

    onSuccess: (updatedTodo) => {
      // Update the specific todo in cache
      queryClient.setQueryData<Todo>(['todos', updatedTodo.id], updatedTodo)

      // Invalidate list to refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  return useMutation({
    mutationFn: async (id: number) => {
      const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
      if (!response.ok) throw new Error('Failed to delete todo')
    },

    onSuccess: (_, deletedId) => {
      queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
        old.filter(todo => todo.id !== deletedId)
      )
    },
  })
}

乐观更新模式:

  1. onMutate:取消查询,保存旧数据快照,并乐观地更新缓存
  2. onError:如果变更失败,则回滚到快照
  3. onSettled:重新获取数据,确保缓存与服务端一致

适用场景:

  • 立即反馈 UI,改善用户体验
  • 低风险变更(待办事项切换、点赞按钮)
  • 避免用于关键数据(付款、账户设置)
步骤 6:设置 DevTools
// Already set up in main.tsx, but here are advanced options:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<ReactQueryDevtools
  initialIsOpen={false}
  buttonPosition="bottom-right"
  position="bottom"

  // Custom toggle button
  toggleButtonProps={{
    style: { marginBottom: '4rem' },
  }}

  // Custom panel styles
  panelProps={{
    style: { height: '400px' },
  }}

  // Only show in dev (already tree-shaken in production)
  // But can add explicit check:
  // {import.meta.env.DEV && <ReactQueryDevtools />}
/>

DevTools 功能:

  • 查看所有查询及其状态
  • 查看查询缓存内容
  • 手动重新获取查询
  • 查看正在执行的变更
  • 检查查询依赖关系
  • 导出状态以进行调试
步骤 7:配置错误边界
// src/components/ErrorBoundary.tsx
import { Component, type ReactNode } from 'react'
import { QueryErrorResetBoundary, useQueryErrorResetBoundary } from '@tanstack/react-query'

type Props = { children: ReactNode }
type State = { hasError: boolean }

class ErrorBoundaryClass extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      )
    }
    return this.props.children
  }
}

// Wrapper with TanStack Query error reset
export function ErrorBoundary({ children }: Props) {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundaryClass onReset={reset}>
          {children}
        </ErrorBoundaryClass>
      )}
    </QueryErrorResetBoundary>
  )
}

// Usage with throwOnError option:
function useTodosWithErrorBoundary() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // Throw errors to error boundary
  })
}

// Or conditional:
function useTodosConditionalError() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: (error, query) => {
      // Only throw server errors, handle network errors locally
      return error instanceof Response && error.status >= 500
    },
  })
}

错误处理策略:

  • 局部处理:使用查询返回的 isError 和 error
  • 全局处理:通过 throwOnError 使用错误边界
  • 混合处理:使用条件式 throwOnError 函数
  • 集中处理:使用 QueryCache 全局错误处理器

关键规则

始终执行

✅ 所有 Hook 都使用对象语法

// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })

✅ 使用数组作为查询键

queryKey: ['todos']              // List
queryKey: ['todos', id]          // Detail
queryKey: ['todos', { filter }]  // Filtered

✅ 适当配置 staleTime

staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches

✅ 初始加载状态使用 isPending

if (isPending) return <Loading />
// isPending = no data yet AND fetching

✅ 在 queryFn 中抛出错误

if (!response.ok) throw new Error('Failed')

✅ 变更后使查询失效

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

✅ 使用 queryOptions 工厂实现可复用模式

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

✅ 使用 gcTime(而非 cacheTime)

gcTime: 1000 * 60 * 60 // 1 hour
绝不执行

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

// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌

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

❌ 绝不使用查询回调(查询中的 onSuccess、onError、onSettled)

// v5 removed these from queries:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {}, // ❌ Removed in v5
})

// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Do something
  }
}, [data])

// Or use mutation callbacks (still supported):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => {}, // ✅ Still works for mutations
})

❌ 绝不使用已弃用的选项

// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead

❌ 绝不认为 isLoading 表示“尚无数据”

// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load

❌ 绝不在无限查询中遗漏 initialPageParam

// v5 requires this:
useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

❌ 绝不将 enabled 与 useSuspenseQuery 配合使用

// Not allowed:
useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not available with suspense
})

// Use conditional rendering instead:
{id && <TodoComponent id={id} />}

预防已知问题

此技能可预防 v5 迁移中的 8 个已记录问题以及常见错误:

问题 #1:必须使用对象语法

错误:useQuery is not a function 或类型错误 来源:v5 迁移指南 发生原因:v5 移除了所有函数重载,只支持对象语法 预防方法:始终使用 useQuery({ queryKey, queryFn, ...options })

之前(v4):

useQuery(['todos'], fetchTodos, { staleTime: 5000 })

之后(v5):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000
})
问题 #2:已移除查询回调

错误:回调不执行,出现 TypeScript 错误 来源:v5 重大变更 发生原因:查询不再支持 onSuccess、onError、onSettled(变更中仍可使用) 预防方法:副作用使用 useEffect,或将逻辑移至变更回调

之前(v4):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  onSuccess: (data) => {
    console.log('Todos loaded:', data)
  },
})

之后(v5):

const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    console.log('Todos loaded:', data)
  }
}, [data])
问题 #3:状态从 Loading 改为 Pending

错误:UI 显示错误的加载状态 来源:v5 迁移:isLoading 更名 发生原因:status: 'loading' 更名为 status: 'pending',isLoading 的含义也已改变 预防方法:初始加载使用 isPending,“待处理且正在获取”使用 isLoading

之前(v4):

const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>

之后(v5):

const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)
问题 #4:cacheTime 改为 gcTime

错误:cacheTime is not a valid option 来源:v5 迁移:gcTime 发生原因:更名为更准确地表示“垃圾回收时间” 预防方法:使用 gcTime,而非 cacheTime

之前(v4):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  cacheTime: 1000 * 60 * 60,
})

之后(v5):

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  gcTime: 1000 * 60 * 60,
})
问题 #5:useSuspenseQuery + enabled

错误:类型错误,enabled 选项不可用 来源:GitHub 讨论 #6206 原因:Suspense 保证数据可用,因此无法有条件地禁用查询 预防方法:使用条件渲染,而不是 enabled 选项

之前(v4/不正确):

useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  enabled: !!id, // ❌ Not allowed
})

之后(v5/正确):

// Conditional rendering:
{id ? (
  <TodoComponent id={id} />
) : (
  <div>No ID selected</div>
)}

// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
  const { data } = useSuspenseQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
    // No enabled option needed
  })
  return <div>{data.title}</div>
}
问题 #6:必须提供 initialPageParam

错误:initialPageParam is required 类型错误 来源:v5 迁移:无限查询 原因:v4 将 undefined 作为第一个 pageParam 传入,v5 则要求提供明确的值 预防方法:始终为无限查询指定 initialPageParam

之前(v4):

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

之后(v5):

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // ✅ Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
问题 #7:已移除 keepPreviousData

错误:keepPreviousData is not a valid option 来源:v5 迁移:placeholderData 原因:已替换为更灵活的 placeholderData 函数 预防方法:使用 placeholderData: keepPreviousData 辅助函数

之前(v4):

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

之后(v5):

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

useQuery({
  queryKey: ['todos', page],
  queryFn: () => fetchTodos(page),
  placeholderData: keepPreviousData,
})
问题 #8:TypeScript 错误类型默认值

错误:错误处理时出现类型错误 来源:v5 迁移:错误类型 原因:v4 使用 unknown,v5 默认使用 Error 类型 预防方法:如果抛出非 Error 类型,请显式指定错误类型

之前(v4 - 错误类型为 unknown):

const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: unknown

之后(v5 - 指定自定义错误类型):

const { error } = useQuery<DataType, string>({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw 'custom error string'
    return data
  },
})
// error: string | null

// Or better: always throw Error objects
const { error } = useQuery({
  queryKey: ['data'],
  queryFn: async () => {
    if (Math.random() > 0.5) throw new Error('custom error')
    return data
  },
})
// error: Error | null (default)

配置文件参考

package.json(完整示例)
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "@tanstack/react-query": "^5.90.5"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.90.2",
    "@tanstack/eslint-plugin-query": "^5.90.2",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.3",
    "vite": "^6.0.1"
  }
}

选择这些版本的原因:

  • React 18.3.1 - 使用 useSyncExternalStore 的必需版本
  • TanStack Query 5.90.5 - 包含所有 v5 修复的最新稳定版本
  • DevTools 5.90.2 - 与 query 版本匹配
  • TypeScript 5.6.3 - 对 query 类型提供最佳类型推断
tsconfig.json(TypeScript 配置)
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* TanStack Query specific */
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}
.eslintrc.cjs(含 TanStack Query 插件的 ESLint 配置)
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@tanstack/eslint-plugin-query/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh', '@tanstack/query'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
  },
}

ESLint 插件可检查:

  • 是否将 query 键作为引用,而不是内联编写
  • 是否缺少 queryFn
  • 是否在 v5 中使用 v4 模式
  • useEffect 中的依赖项是否有误

常见模式

模式 1:依赖查询
// Fetch user, then fetch user's posts
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, // Only fetch posts after user is loaded
  })

  if (!user) return <div>Loading user...</div>
  if (!posts) return <div>Loading posts...</div>

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

适用场景:查询 B 依赖查询 A 返回的数据

模式 2:使用 useQueries 并行查询
// Fetch multiple todos in parallel
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>Loading...</div>
  if (isError) return <div>Error loading 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 minutes
    })
  }

  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>Loading more...</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, // Only search if 3+ characters
  })

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

工作原理:

  • queryKey 发生变化时,之前的查询会自动取消
  • 将 signal 传递给 fetch,以便正确清理
  • 浏览器会中止尚未完成的 fetch 请求

使用捆绑资源

模板(templates/)

完整且可直接复制的代码示例:

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

使用示例:

# Copy query client config
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/

# Copy provider setup
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx
参考资料(references/)

按需加载的深入文档:

  • v4-to-v5-migration.md - 完整的 v4 → v5 迁移指南
  • best-practices.md - 请求瀑布、缓存策略、性能
  • common-patterns.md - 可复用查询、乐观更新、无限滚动
  • typescript-patterns.md - 类型安全、泛型、类型推断
  • testing.md - 使用 MSW、React Testing Library 进行测试
  • top-errors.md - 所有 8+ 个错误及解决方案

Claude 应何时加载这些文档:

  • 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 转换数据
// Only subscribe to specific slice of data
function TodoCount() {
  const { data: count } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.length, // Only re-render when count changes
  })

  return <div>Total todos: {count}</div>
}

// Transform data shape
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>
  )
}

优势:

  • 仅当所选数据发生变化时,组件才会重新渲染
  • 减少内存使用量(组件状态中存储的数据更少)
  • 保持查询缓存不变(其他组件仍可获取完整数据)
请求瀑布(反模式)
// ❌ BAD: Sequential waterfalls
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,
  })

  // Each query waits for previous one = slow!
}

// ✅ GOOD: Fetch in parallel when possible
function GoodUserProfile({ userId }: { userId: number }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  })

  // Fetch posts AND comments in parallel
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId), // Don't wait for user
  })

  const { data: comments } = useQuery({
    queryKey: ['comments', userId],
    queryFn: () => fetchUserComments(userId), // Don't wait for posts
  })

  // All 3 queries run in parallel = fast!
}
服务端状态与客户端状态
// ❌ Don't use TanStack Query for client-only state
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)

// ✅ Use useState for client state
const [isModalOpen, setIsModalOpen] = useState(false)

// ✅ Use TanStack Query for server state
const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

经验法则:

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

依赖项

必需:

推荐:

可选:

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

官方文档


软件包版本(已于 2025-10-22 验证)

{
  "dependencies": {
    "@tanstack/react-query": "^5.90.5"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.90.2",
    "@tanstack/eslint-plugin-query": "^5.90.2"
  }
}

验证方式:

  • npm view @tanstack/react-query version → 5.90.5
  • npm view @tanstack/react-query-devtools version → 5.90.2
  • 上次检查:2025-10-22

生产环境示例

此技能基于以下生产环境模式:

  • 构建时间:约 6 小时的研究与开发
  • 避免的错误:8 个(所有已记录的 v5 迁移问题)
  • 令牌效率:相比手动设置节省约 65%
  • 验证:✅ 已使用 TypeScript strict mode 测试所有模式

故障排除

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

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

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

// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)
问题:查询中的回调(onSuccess、onError)不起作用

解决方案:这些回调已在 v5 中移除。请使用 useEffect 或将其移至 mutations:

// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
  if (data) {
    // Handle success
  }
}, [data])

// ✅ For mutations (still work):
useMutation({
  mutationFn: addTodo,
  onSuccess: () => { /* ... */ },
})
问题:初次加载期间 isLoading 始终为 false

解决方案:改用 isPending:

const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progress
问题:无法识别 cacheTime 选项

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

gcTime: 1000 * 60 * 60 // 1 hour
问题:使用 useSuspenseQuery 并设置 enabled 选项时出现类型错误

解决方案:suspense 不支持 enabled。请使用条件渲染:

{id && <TodoComponent id={id} />}
问题:mutation 后数据未重新获取

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

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] })
}
问题:无限查询需要 initialPageParam

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

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0, // Required
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
问题:keepPreviousData 无效

解决方案:已替换为 placeholderData:

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

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

完整设置清单

使用此清单验证设置:

  • 已安装 @tanstack/[email protected]+
  • 已安装 @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

评论

支持 Markdown