mcpField
T
tanstack-query

tanstack-query

github.com/jezweb开发1.1k2.5k 次安装3 周前更新
代码仓库

关于 tanstack-query

面向 React 应用的 TanStack Query v5 技能指南,涵盖查询与变更模式、版本迁移要点及常见问题的规避方法。

主要功能

  1. 01查询与变更 Hook 模式
  2. 02乐观更新与变更状态跟踪
  3. 03支持离线的网络模式
  4. 04并行查询与无限查询指南
  5. 05v5 迁移问题及处理方式

适用场景

  1. 01构建 React 数据获取流程
  2. 02跨组件跟踪待处理变更
  3. 03离线时使用缓存查询
  4. 04将应用从 v4 迁移至 v5

安装

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

$npx skills add https://github.com/jezweb/claude-skills --skill tanstack-query

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
best-practices.md
common-patterns.md
example-reference.md
testing.md
top-errors.md
typescript-patterns.md
v4-to-v5-migration.md
rules/
tanstack-query.md
scripts/
example-script.sh
templates/
custom-hooks-pattern.tsx
devtools-setup.tsx
error-boundary.tsx
package.json
provider-setup.tsx
query-client-config.ts
use-infinite-query.tsx
use-mutation-basic.tsx
use-mutation-optimistic.tsx
use-query-basic.tsx

详细介绍

TanStack Query(React Query)v5

最后更新:2026-01-20 版本:@tanstack/[email protected]、@tanstack/[email protected] 要求:React 18.0+(useSyncExternalStore)、TypeScript 4.7+(推荐)


v5 新功能

useMutationState - 跨组件变更跟踪

无需逐层传递 props,即可从任何位置访问变更状态:

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

function GlobalLoadingIndicator() {
  // Get all pending mutations
  const pendingMutations = useMutationState({
    filters: { status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  if (pendingMutations.length === 0) return null
  return <div>Saving {pendingMutations.length} items...</div>
}

// Filter by mutation key
const todoMutations = useMutationState({
  filters: { mutationKey: ['addTodo'] },
})
简化乐观更新

使用 variables 的新模式——无需操作缓存,也无需回滚:

function TodoList() {
  const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

  const addTodo = useMutation({
    mutationKey: ['addTodo'],
    mutationFn: (newTodo) => api.addTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  // Show optimistic UI using variables from pending mutations
  const pendingTodos = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables,
  })

  return (
    <ul>
      {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
      {/* Show pending items with visual indicator */}
      {pendingTodos.map((todo, i) => (
        <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
      ))}
    </ul>
  )
}
throwOnError - 错误边界

从 useErrorBoundary 重命名而来(存在破坏性变更):

import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
          <div>
            Error! <button onClick={resetErrorBoundary}>Retry</button>
          </div>
        )}>
          <Todos />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  )
}

function Todos() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    throwOnError: true, // ✅ v5 (was useErrorBoundary in v4)
  })
  return <div>{data.map(...)}</div>
}
网络模式(离线/PWA 支持)

控制离线时的行为:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // Use cache when offline
    },
  },
})

// Per-query override
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  networkMode: 'always', // Always try, even offline (for local APIs)
})
模式行为
online(默认)仅在联网时获取
always始终尝试(适用于本地 API/Service Worker API)
offlineFirst优先使用缓存,联网后再获取

检测暂停状态:

const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network
带 Combine 的 useQueries

合并并行查询的结果:

const results = 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),
    error: results.find(r => r.error)?.error,
  }),
})

// Access combined result
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]
infiniteQueryOptions 辅助函数

用于无限查询的类型安全工厂(与 queryOptions 相对应):

import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'

const todosInfiniteOptions = infiniteQueryOptions({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Reuse across hooks
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)
maxPages - 内存优化

限制无限查询在缓存中存储的页面数:

useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages
  maxPages: 3, // Only keep 3 pages in memory
})

注意:maxPages 要求双向分页(getNextPageParam 和 getPreviousPageParam)。


快速设置

npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
第 2 步:Provider + 配置
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 min
      gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)
      refetchOnWindowFocus: false,
    },
  },
})

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
第 3 步:查询和变更 Hook
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

// Query options factory (v5 pattern)
export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    if (!res.ok) throw new Error('Failed to fetch')
    return res.json()
  },
})

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

export function useAddTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (newTodo) => {
      const res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      })
      if (!res.ok) throw new Error('Failed to add')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })
}

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

  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>
}

关键规则

始终遵循

✅ 所有 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} />}

❌ 绝不依赖 refetchOnMount: false 处理出错的查询

// Doesn't work - errors are always stale
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,  // ❌ Ignored when query has error
})

// Use retryOnMount instead
useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch for errored queries
  retry: 0,
})

已知问题预防

此技能可预防 v5 迁移、SSR/水合错误和常见错误中记录的 16 个问题:

问题 #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)
问题 #9:流式服务器组件水合错误

错误:Hydration failed because the initial UI does not match what was rendered on the server 来源:GitHub 问题 #9642 影响范围:使用流式 SSR(void 预取模式)的 v5.82.0+ 原因:发生竞态条件,hydrate() 同步完成解析,但 query.fetch() 会创建异步重试器,导致服务器端与客户端的 isFetching/isStale 不一致 预防方法:使用 useSuspenseQuery 和流式预取时,不要基于 fetchStatus 进行条件渲染;或者等待预取完成,而不要使用 void 模式

之前(会导致水合错误):

// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;

之后(临时解决方案):

// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>;  // No conditional on isFetching

状态:已知问题,维护者正在调查。需要在 useSyncExternalStore 中实现 getServerSnapshot。

问题 #10:预取时 useQuery 水合错误

错误:水合期间文本内容不匹配 来源:GitHub 问题 #9399 影响范围:使用服务器端预取的 v5.x 原因:tryResolveSync 会检测 RSC 负载中已解析的 Promise,并在水合期间同步提取数据,从而绕过正常的 pending 状态 预防方法:SSR 时使用 useSuspenseQuery 替代 useQuery,或者避免基于 isLoading 进行条件渲染

之前(会导致水合错误):

// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Client Component
function Todos() {
  const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  if (isLoading) return <div>Loading...</div>;  // Server renders this
  return <div>{data.length} todos</div>;  // Client hydrates with this
}

之后(临时解决方案):

// Use useSuspenseQuery instead
function Todos() {
  const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
  return <div>{data.length} todos</div>;
}

状态:“在我的 OSS 待修复事项列表中排在最前面”——维护者 Ephem(2025 年 11 月)。需要在 useSyncExternalStore 中实现 getServerSnapshot。

问题 #11:Errored 查询未遵循 refetchOnMount

错误:尽管设置了 refetchOnMount: false,查询仍会在挂载时重新获取 来源:GitHub Issue #10018 影响版本:v5.90.16+ 原因:没有数据的 Errored 查询始终会被视为过期。这是有意为之,以避免错误状态永久显示 预防方法:使用 retryOnMount: false 代替(或与)refetchOnMount: false

修改前(尽管设置了选项,仍会重新获取):

const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => { throw new Error('Fails') },
  refetchOnMount: false,  // Ignored when query is in error state
  retry: 0,
});
// Query refetches every time component mounts

修改后(正确):

const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: failingFetch,
  refetchOnMount: false,
  retryOnMount: false,  // ✅ Prevents refetch on mount for errored queries
  retry: 0,
});

状态:已记录的行为(有意为之)。retryOnMount 这个名称略有误导——它控制 Errored 查询是否会在挂载时触发新的获取,而非自动重试。

问题 #12:Mutation 回调签名的破坏性变更(v5.89.0)

错误:Mutation 回调中出现 TypeScript 错误 来源:GitHub Issue #9660 影响版本:v5.89.0+ 原因:在 variables 和 context 之间添加了 onMutateResult 参数,导致回调签名从 3 个参数变为 4 个 预防方法:更新所有 Mutation 回调,使其接受 4 个参数而非 3 个

修改前(v5.88 及更早版本):

useMutation({
  mutationFn: addTodo,
  onError: (error, variables, context) => {
    // context is now onMutateResult, missing final context param
  },
  onSuccess: (data, variables, context) => {
    // Same issue
  }
});

修改后(v5.89.0+):

useMutation({
  mutationFn: addTodo,
  onError: (error, variables, onMutateResult, context) => {
    // onMutateResult = return value from onMutate
    // context = mutation function context
  },
  onSuccess: (data, variables, onMutateResult, context) => {
    // Correct signature with 4 parameters
  }
});

注意:如果不使用 onMutate,onMutateResult 参数将为 undefined。此破坏性变更是在补丁版本中引入的。

问题 #13:只读查询键导致部分匹配失效(v5.90.8)

错误:Type 'readonly ["todos", string]' is not assignable to type '["todos", string]' 来源:GitHub Issue #9871 | 已在 PR #9872 中修复 影响版本:仅 v5.90.8(已在 v5.90.9 中修复) 原因:部分查询匹配导致只读查询键(使用 as const)的 TypeScript 类型失效 预防方法:升级到 v5.90.9+,或在必须使用 v5.90.8 时进行类型断言

修改前(v5.90.8 - TypeScript 错误):

export function todoQueryKey(id?: string) {
  return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']

useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: todoQueryKey('123')
      // Error: readonly ['todos', string] not assignable to ['todos', string]
    });
  }
});

修改后(v5.90.9+):

// Works correctly with readonly types
queryClient.invalidateQueries({
  queryKey: todoQueryKey('123')  // ✅ No type error
});

状态:已在 v5.90.9 中修复。使用 openapi-react-query 等代码生成器(会生成只读查询键)的用户尤其容易受到影响。

问题 #14:useMutationState 丢失类型推断

错误:mutation.state.variables 的类型为 unknown,而非实际类型 来源:GitHub Issue #9825 影响版本:所有 v5.x 版本 原因:模糊的 Mutation 键匹配无法保证类型推断(与 queryClient.getQueryCache().find() 存在相同问题) 预防方法:在 select 回调中显式进行类型转换

修改前(类型推断不起作用):

const addTodo = useMutation({
  mutationKey: ['addTodo'],
  mutationFn: (todo: Todo) => api.addTodo(todo),
});

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => {
    return mutation.state.variables;  // Type: unknown
  },
});

修改后(显式类型转换):

const pendingTodos = useMutationState({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>

状态:模糊匹配的已知限制。暂无修复计划。

问题 #15:StrictMode 下使用 fetchQuery 时查询被取消

错误:将 fetchQuery() 与 useQuery 搭配使用时出现 CancelledError 来源:GitHub Issue #9798 影响范围:仅开发环境(React StrictMode) 原因:StrictMode 会导致组件重复挂载和卸载。当 useQuery 卸载且它是最后一个观察者时,即使 fetchQuery() 也在运行,查询仍会被取消 预防方法:这是预期的、仅发生于开发环境的行为,不会影响生产环境

示例:

async function loadData() {
  try {
    const data = await queryClient.fetchQuery({
      queryKey: ['data'],
      queryFn: fetchData,
    });
    console.log('Loaded:', data);  // Never logs in StrictMode
  } catch (error) {
    console.error('Failed:', error);  // CancelledError
  }
}

function Component() {
  const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
  // In StrictMode, component unmounts/remounts, cancelling fetchQuery
}

解决方法:

// Keep query observed with staleTime
const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  staleTime: Infinity,  // Keeps query active
});

状态:这是预期的 StrictMode 行为,并非 Bug。生产构建不受影响。

问题 #16:invalidateQueries 仅重新获取活跃查询

错误:调用 invalidateQueries() 后,非活跃查询没有重新获取 来源:GitHub Issue #9531 影响范围:所有 v5.x 版本 原因:文档具有误导性——默认情况下,invalidateQueries() 仅重新获取“活跃”查询,而非“所有”查询 预防方法:使用 refetchType: 'all' 强制重新获取非活跃查询

默认行为:

// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });

重新获取非活跃查询:

queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'all'  // Refetch active AND inactive
});

状态:文档已修正,明确说明“活跃”查询。这是预期行为。


社区提示

注意:这些提示来自社区专家和维护者的博客。请根据你使用的版本进行核实。

提示:多个监听器共用查询选项

来源:TkDodo's Blog - API Design Lessons | 可信度:高 适用版本:v5.27.3+

多个组件使用同一查询但设置了不同选项(例如 staleTime)时,“后写入者胜出”规则适用于后续获取,但当前正在进行的查询仍使用其原始选项。如果组件在不同时间挂载,可能会导致意外行为。

意外行为示例:

// Component A mounts first
function ComponentA() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 5000,  // Applied initially
  });
}

// Component B mounts while A's query is in-flight
function ComponentB() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 60000,  // Won't affect current fetch, only future ones
  });
}

推荐做法:

// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: getStaleTime(),  // Evaluated on each render
});
提示:refetch() 不适用于参数变更

来源:Avoiding Common Mistakes with TanStack Query | 可信度:高

refetch() 函数只应用于使用相同参数刷新(例如手动点击“重新加载”按钮)。对于新参数(筛选条件、页码、搜索词等),应将它们加入查询键。

反模式:

// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
  queryKey: ['todos'],  // Same key for all pages
  queryFn: () => fetchTodos(page),
});

// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>

正确模式:

// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
  queryKey: ['todos', page],  // Key changes with page
  queryFn: () => fetchTodos(page),
  // Query automatically refetches when page changes
});

<button onClick={() => setPage(2)}>Next</button>  // Just update state

何时使用 refetch():

// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

<button onClick={() => refetch()}>Refresh</button>  // Same parameters

关键模式

依赖查询(查询 B 等待查询 A):

const { data: posts } = useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
  enabled: !!user, // Wait for user
})

并行查询(同时获取多项数据):

const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})

预取(悬停时预加载):

queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })

无限滚动(useInfiniteQuery):

useInfiniteQuery({
  queryKey: ['todos', 'infinite'],
  queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
  initialPageParam: 0, // Required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

取消查询(queryKey 变更时自动取消):

queryFn: async ({ signal }) => {
  const res = await fetch(`/api/todos?q=${search}`, { signal })
  return res.json()
}

数据转换(select):

select: (data) => data.filter(todo => todo.completed)

避免请求瀑布:尽可能并行获取(除非确实存在依赖关系,否则不要串联查询)


官方文档:https://tanstack.com/query/latest | v5 迁移指南:https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub:https://github.com/TanStack/query | Context7:/websites/tanstack_query

评论

支持 Markdown