Token导航 LogoToken导航

Agent Skill · 开发

tanstack-query(tanstack 查询)

tanstack-query 用于补充开发相关能力,适合在 Local Agent 中需要让 Agent 承接开发相关任务时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

tanstack-query 是开发类 Agent Skill,可用宿主为 Local Agent,通过unknown获取,权限为操作浏览器;许可证信息还不明确,第三方扫描已通过,按安装量在开发类的 21,746 个 Skill 里排第 10,879。

总安装 2,040周安装 85下载 680核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:tanstack-query(tanstack 查询)
来源仓库:https://smithery.ai
仓库路径:tanstack-query
安装命令:
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。当前暂无明确安装命令,请以来源页面说明为准。

这个 Skill 是做什么的

开发类 Skill 面向写代码、改代码和排查问题,一般会读写仓库文件,部分还会执行构建或测试命令。

tanstack-query 用于补充开发相关能力。适合在 Local Agent 中处理开发相关任务时使用。可结合来源仓库和原始 README 进一步核验具体用法。安装前建议确认权限范围和维护状态。tanstack-query 属于开发类 Skill,可作为该场景下的辅助能力补充。

注意是否会触发联网、命令执行或文件读写。

来源仓库内路径:tanstack-query

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Local Agent;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    没有现成安装命令,按unknown的说明从来源页面获取,仓库路径是 tanstack-query。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要操作浏览器,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

tanstack-query 怎么安装?

公开资料没有给出可以直接复制的命令,需要按来源页面说明手动安装,仓库内路径为 tanstack-query。

tanstack-query 支持哪些 AI 工具?

公开资料标注支持 Local Agent。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

tanstack-query 需要什么权限?

权限级别是操作浏览器。该 Skill 可能涉及浏览器控制能力,使用时可能读取或操作网页内容,需要在受控环境中确认权限边界。

tanstack-query 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

tanstack-query 用的人多吗?

公开数据是累计安装 2,040 次,近一周安装 85 次,下载 680 次。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

tanstack-query 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 专注 Local Agent 一种宿主,触发方式和使用习惯不用来回切换。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Local Agent,想直接扩展它能力的人。
  • 需要处理开发类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

开发类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
skill-vetter

技能审查员 · OpenClaw

8,779,018986操作浏览器MIT-0通过
ontology

本体论 · OpenClaw

6,636,960576只读MIT-0通过
entra-app-registration

内部应用程序注册 · Codex、Claude

6,631,872764接触敏感数据未明确通过
entra-app-registration

内部应用程序注册 · Codex、Claude

3,757,680201接触敏感数据未明确通过
nano-banana-pro

纳米香蕉专业版 · OpenClaw

3,526,898364接触敏感数据MIT-0有提醒
tanstack-query

tanstack 查询 · 本页

2,040—操作浏览器未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

TanStack Query v5 - Complete Guide

TanStack Query v5 (October 2023) is the async state manager for this project. It requires React 18+, features first-class Suspense support, improved TypeScript inference, and a 20% smaller bundle. This section covers production-ready patterns based on official documentation and community best practices.

Breaking Changes in v5

Key updates you need to know:

  1. Single Object Signature: All hooks now accept one configuration object: // ✅ v5 - single object useQuery({queryKey, queryFn,...options}) // ❌ v4 - multiple overloads (deprecated) useQuery(queryKey, queryFn, options)
  2. Renamed Options:

- cacheTime → gcTime (garbage collection time) - keepPreviousData → placeholderData: keepPreviousData - isLoading now means isPending && isFetching

  1. Callbacks Removed from useQuery:

- onSuccess, onError, onSettled removed from useQuery - Use global QueryCache callbacks instead - Prevents duplicate executions

  1. Infinite Queries Require initialPageParam:

- No default value provided - Must explicitly set initialPageParam (e.g., 0 or null)

  1. First-Class Suspense:

- New dedicated hooks: useSuspenseQuery, useSuspenseInfiniteQuery - No experimental flag needed - Data is never undefined at type level

Migration: Use the official codemod for automatic migration: npx @tanstack/query-codemods v5/replace-import-specifier

Smart Defaults

Query v5 ships with production-ready defaults:

{
  staleTime: 0,              // Data instantly stale (refetch on mount)
  gcTime: 5 * 60_000,        // Keep unused cache for 5 minutes
  retry: 3,                  // 3 retries with exponential backoff
  refetchOnWindowFocus: true,// Refetch when user returns to tab
  refetchOnReconnect: true,  // Refetch when network reconnects
}

Philosophy: React Query is an async state manager, not a data fetcher. You provide the Promise; Query manages caching, background updates, and synchronization.

Client Setup

// src/app/providers.tsx
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'
import { toast } from './toast' // Your notification system

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 0,          // Adjust per-query
      gcTime: 5 * 60_000,    // 5 minutes (v5: formerly cacheTime)
      retry: (failureCount, error) => {
        // Don't retry on 401 (authentication errors)
        if (error?.response?.status === 401) return false
        return failureCount < 3
      },
    },
  },
  queryCache: new QueryCache({
    onError: (error, query) => {
      // Only show toast for background errors (when data exists)
      if (query.state.data !== undefined) {
        toast.error(`Something went wrong: ${error.message}`)
      }
    },
  }),
})

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

DevTools Setup (auto-excluded in production):

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

<QueryClientProvider client={queryClient}>
  {children}
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

Server-Side Rendering Configuration

When using TanStack Query with SSR (Next.js, Remix, TanStack Start), configure server-specific defaults:

// Server-side QueryClient configuration
export function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // Server: Don't retry on server (fail fast)
        retry: typeof window === 'undefined' ? 0 : 3,
        // Server: Data is always fresh when rendered
        staleTime: 60_000, // 1 minute
      },
    },
  })
}

Server vs Client Defaults:

OptionClient DefaultServer RecommendedWhy
retry30Server should fail fast, not retry loops
staleTime060_000+Server-rendered data is fresh
gcTime5 minInfinityNo garbage collection needed on server
refetchOnWindowFocustruefalseNo window on server
refetchOnReconnecttruefalseNo reconnect on server

Important: In SPA-only apps (TanStack Router + Vite), you don't need these server defaults. They're only relevant for SSR frameworks.

Streaming SSR (Experimental)

For Next.js App Router, @tanstack/react-query-next-experimental enables streaming:

pnpm add @tanstack/react-query-next-experimental

Setup:

// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { staleTime: 60_000 },
    },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (typeof window === 'undefined') {
    return makeQueryClient() // Server: always new
  }
  return (browserQueryClient ??= makeQueryClient()) // Browser: singleton
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
    </QueryClientProvider>
  )
}

Usage in Client Components:

'use client'

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

export function UserProfile({ userId }: { userId: string }) {
  // No prefetch needed! Data streams from server
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  return <div>{user.name}</div>
}

Benefits:

  • Skip manual prefetching in Server Components
  • Data streams to client as it resolves
  • Suspense boundaries show loading states naturally

Limitations:

  • Next.js App Router only (experimental)
  • Not for TanStack Router SPAs (use route loaders instead)

Server Components Integration

When you have React Server Components (RSC), how does TanStack Query fit?

The Mental Model

Think of Server Components as another framework loader (like route loaders):

FeatureServer ComponentsTanStack Query
Initial data fetchYes (server)Yes (client prefetch)
Client mutationsNoYes
Background refetchNoYes
Optimistic updatesNoYes
Real-time updatesNoYes
Cache managementNoYes
When TanStack Query is Still Valuable

Even in RSC-heavy apps, Query remains essential for:

  1. Client-Side Mutations // Server Component fetches, Client handles mutations export default async function PostPage({params}) {const post = await fetchPost(params.id) // Server fetch return <PostWithComments post={post} /> // Client mutations} 'use client' function PostWithComments({post}) {const addComment = useMutation({...}) // Still need Query! //...}
  2. Background Refetching After Initial Load // Initial: Server Component renders with fresh data // After: Query keeps data fresh on client
  3. Optimistic Updates // Can't do optimistic updates with Server Components alone const likeMutation = useMutation({mutationFn: likePost, onMutate: async () => {// Optimistic update - only possible with Query},})
  4. Real-Time Updates // WebSocket data, polling, etc. - client-only useQuery({queryKey: ['notifications'], queryFn: fetchNotifications, refetchInterval: 30_000, // Real-time polling})
Recommended Pattern
// Server Component: Initial fetch
export default async function DashboardPage() {
  const initialData = await fetchDashboard()

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

// Client Component: Mutations + real-time
'use client'
function DashboardClient({ initialData }) {
  // Query hydrates from server data, then manages client state
  const { data } = useQuery({
    queryKey: ['dashboard'],
    queryFn: fetchDashboard,
    initialData,
  })

  const updateWidget = useMutation({ ... })
  // ...
}
SPA Recommendation

For SPA-only apps (TanStack Router + Vite): Server Components don't apply. Use TanStack Query as your primary data layer with route loaders for prefetching.

Query + React 19 Actions

When using React 19 Actions alongside Query, keep responsibilities clear:

Complementary Usage
// Query: Fetching and caching
const { data: posts } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

// Action: Form submission with built-in validation
async function createPostAction(formData: FormData) {
  'use server'
  const result = await createPost(formData)
  return result
}

// After action succeeds, invalidate Query cache
const [state, formAction] = useActionState(async (prev, formData) => {
  const result = await createPostAction(formData)
  if (result.success) {
    queryClient.invalidateQueries({ queryKey: ['posts'] })
  }
  return result
}, { success: false })
Decision Matrix
Use CaseRecommendation
Data fetchingQuery (useQuery)
List cachingQuery
Form submissionAction (useActionState) + Query invalidation
Button click mutationQuery (useMutation)
Optimistic update with rollbackQuery (useMutation)
Server-side validationAction
Complex multi-step mutationsQuery (useMutation)
Rule of Thumb
  • Actions for form submissions with server-side validation
  • Query for everything else (fetching, caching, complex mutations)
  • Both when forms need to update Query cache after success

Architecture: Feature-Based Colocation

Recommended pattern: Group queries with related features, not by file type.

src/features/
├── Todos/
│   ├── index.tsx           # Feature entry point
│   ├── queries.ts          # All React Query logic (keys, functions, hooks)
│   ├── types.ts            # TypeScript types
│   └── components/         # Feature-specific components

Export only custom hooks from query files. Keep query functions and keys private:

// features/todos/queries.ts

// 1. Query Key Factory (hierarchical structure)
const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
}

// 2. Query Function (private)
const fetchTodos = async (filters: string): Promise<Todo[]> => {
  const response = await axios.get('/api/todos', { params: { filters } })
  return response.data
}

// 3. Custom Hook (public API)
export const useTodosQuery = (filters: string) => {
  return useQuery({
    queryKey: todoKeys.list(filters),
    queryFn: () => fetchTodos(filters),
    staleTime: 30_000, // Fresh for 30 seconds
  })
}

Benefits:

  • Prevents key/function mismatches
  • Clean public API
  • Encapsulation and maintainability
  • Easy to locate all query logic for a feature

Query Key Factories (Essential)

Structure keys hierarchically from generic to specific:

// ✅ Correct hierarchy
['todos']                          // Invalidates everything
['todos', 'list']                  // Invalidates all lists
['todos', 'list', { filters }]     // Invalidates specific list
['todos', 'detail', 1]             // Invalidates specific detail

// ❌ Wrong - flat structure
['todos-list-active']              // Can't partially invalidate

Critical rule: Query keys must include ALL variables used in queryFn. Treat query keys like dependency arrays:

// ✅ Correct - includes all variables
const { data } = useQuery({
  queryKey: ['todos', filters, sortBy],
  queryFn: () => fetchTodos(filters, sortBy),
})

// ❌ Wrong - missing variables
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: () => fetchTodos(filters, sortBy), // filters/sortBy not in key!
})

Type consistency matters: ['todos', '1'] and ['todos', 1] are different keys. Be consistent with types.

Query Options API (Type Safety)

The modern pattern for maximum type safety across your codebase:

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

function todoOptions(id: number) {
  return queryOptions({
    queryKey: ['todos', id],
    queryFn: () => fetchTodo(id),
    staleTime: 5000,
  })
}

// ✅ Use everywhere with full type safety
useQuery(todoOptions(1))
queryClient.prefetchQuery(todoOptions(5))
queryClient.setQueryData(todoOptions(42).queryKey, newTodo)
queryClient.getQueryData(todoOptions(42).queryKey) // Fully typed!

Benefits:

  • Single source of truth for query configuration
  • Full TypeScript inference for imperatively accessed data
  • Reusable across hooks and imperative methods
  • Prevents key/function mismatches

Data Transformation Strategies

Choose the right approach based on your use case:

1. Transform in queryFn - Simple cases where cache should store transformed data:

const fetchTodos = async (): Promise<Todo[]> => {
  const response = await axios.get('/api/todos')
  return response.data.map(todo => ({
    ...todo,
    name: todo.name.toUpperCase()
  }))
}

2. Transform with select option (RECOMMENDED) - Enables partial subscriptions:

// Only re-renders when filtered data changes
export const useTodosQuery = (filters: string) =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.filter(todo => todo.status === filters),
  })

// Only re-renders when count changes
export const useTodosCount = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (data) => data.length,
  })

⚠️ Memoize select functions to prevent running on every render:

// ✅ Stable reference
const transformTodos = (data: Todo[]) => expensiveTransform(data)

const query = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: transformTodos, // Stable function reference
})

// ❌ Runs on every render
const query = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data) => expensiveTransform(data), // New function every render
})

TypeScript Best Practices

Let TypeScript infer types from queryFn rather than specifying generics:

// ✅ Recommended - inference
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos, // Returns Promise<Todo[]>
})
// data is Todo[] | undefined

// ❌ Unnecessary - explicit generics
const { data } = useQuery<Todo[]>({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

Discriminated unions automatically narrow types:

const { data, isSuccess, isError, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

if (isSuccess) {
  // data is Todo[] (never undefined)
}

if (isError) {
  // error is defined
}

Use queryOptions helper for maximum type safety across imperative methods.

Custom Hooks Pattern

Always create custom hooks even for single queries:

// ✅ Recommended - custom hook with encapsulation
export function usePost(
  id: number,
  options?: Omit<UseQueryOptions<Post>, 'queryKey' | 'queryFn'>
) {
  return useQuery({
    queryKey: ['posts', id],
    queryFn: () => getPost(id),
    ...options,
  })
}

// Usage: allows callers to override any option except key/fn
const { data } = usePost(42, { staleTime: 10_000 })

Benefits:

  • Centralizes query logic
  • Easy to update all usages
  • Consistent configuration
  • Better testing

Error Handling (Multi-Layer Strategy)

Layer 1: Component-Level - Specific user feedback:

function TaskList() {
  const { data, error, isError, isLoading } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isLoading) return <Spinner />
  if (isError) return <ErrorAlert>{error.message}</ErrorAlert>

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

Layer 2: Global Error Handling - Background errors via QueryCache:

// Already configured in client setup above
queryCache: new QueryCache({
  onError: (error, query) => {
    if (query.state.data !== undefined) {
      toast.error(`Background error: ${error.message}`)
    }
  },
})

Layer 3: Error Boundaries - Catch render errors:

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

<QueryErrorResetBoundary>
  {({ reset }) => (
    <ErrorBoundary
      onReset={reset}
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <p>Error: {error.message}</p>
          <button onClick={resetErrorBoundary}>Try again</button>
        </div>
      )}
    >
      <TaskList />
    </ErrorBoundary>
  )}
</QueryErrorResetBoundary>

Suspense Integration

First-class Suspense support in v5 with dedicated hooks:

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

function TaskList() {
  // data is NEVER undefined (type-safe)
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

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

// Wrap with Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <TaskList />
    </Suspense>
  )
}

Benefits:

  • Eliminates loading state management
  • Data always defined (TypeScript enforced)
  • Cleaner component code
  • Works with React.lazy for code-splitting

Mutations with Optimistic Updates

Basic mutation with cache invalidation:

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

  return useMutation({
    mutationFn: (newTodo: CreateTodoDTO) =>
      api.post('/todos', newTodo).then(res => res.data),
    onSuccess: (data) => {
      // Set detail query immediately
      queryClient.setQueryData(['todos', data.id], data)
      // Invalidate list queries
      queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
    },
  })
}

Simple optimistic updates using variables:

const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/todos', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const { isPending, variables, mutate } = addTodoMutation

return (
  <ul>
    {todoQuery.data?.map(todo => <li key={todo.id}>{todo.text}</li>)}
    {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
  </ul>
)

Advanced optimistic updates with rollback:

useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Cancel outgoing queries (prevent race conditions)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Snapshot current data
    const previousTodos = queryClient.getQueryData(['todos'])

    // Optimistically update cache
    queryClient.setQueryData(['todos'], (old: Todo[]) =>
      old?.map(todo => todo.id === newTodo.id ? newTodo : todo)
    )

    // Return context for rollback
    return { previousTodos }
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos'], context?.previousTodos)
    toast.error('Update failed. Changes reverted.')
  },
  onSettled: () => {
    // Always refetch to ensure consistency
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

Key principles:

  • Cancel ongoing queries in onMutate to prevent race conditions
  • Snapshot previous data before updating
  • Restore snapshot on error
  • Always invalidate in onSettled for eventual consistency
  • Never mutate cached data directly - always use immutable updates

Authentication Integration

Handle token refresh at HTTP client level (not React Query):

// src/lib/api-client.ts
import axios from 'axios'
import createAuthRefreshInterceptor from 'axios-auth-refresh'

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
})

// Add token to requests
apiClient.interceptors.request.use((config) => {
  const token = getAccessToken()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// Refresh token on 401
const refreshAuth = async (failedRequest: any) => {
  try {
    const newToken = await fetchNewToken()
    failedRequest.response.config.headers.Authorization = `Bearer ${newToken}`
    setAccessToken(newToken)
    return Promise.resolve()
  } catch {
    removeAccessToken()
    window.location.href = '/login'
    return Promise.reject()
  }
}

createAuthRefreshInterceptor(apiClient, refreshAuth, {
  statusCodes: [401],
  pauseInstanceWhileRefreshing: true,
})

Protected queries use the enabled option:

const useTodos = () => {
  const { user } = useUser() // Get current user from auth context

  return useQuery({
    queryKey: ['todos', user?.id],
    queryFn: () => fetchTodos(user.id),
    enabled: !!user, // Only execute when user exists
  })
}

On logout: Clear the entire cache with queryClient.clear() (not invalidateQueries() which triggers refetches):

const logout = () => {
  removeAccessToken()
  queryClient.clear() // Clear all cached data
  navigate('/login')
}

Advanced Patterns

Prefetching - Eliminate loading states:

// Hover prefetching
function ShowDetailsButton() {
  const queryClient = useQueryClient()

  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['details'],
      queryFn: getDetailsData,
      staleTime: 60_000, // Consider fresh for 1 minute
    })
  }

  return (
    <button onMouseEnter={prefetch} onClick={showDetails}>
      Show Details
    </button>
  )
}

// Route-level prefetching (see Router × Query Integration section)

Infinite Queries - Infinite scrolling/pagination:

function Projects() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: ({ pageParam }) => fetchProjects(pageParam),
    initialPageParam: 0, // Required in v5
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  })

  if (isLoading) return <Spinner />

  return (
    <>
      {data.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.data.map(project => (
            <ProjectCard key={project.id} {...project} />
          ))}
        </React.Fragment>
      ))}

      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </>
  )
}

Offset-Based Pagination with placeholderData:

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

function Posts() {
  const [page, setPage] = useState(0)

  const { data, isPending, isPlaceholderData } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    placeholderData: keepPreviousData, // Show previous data while fetching
  })

  return (
    <>
      {data.posts.map(post => <PostCard key={post.id} {...post} />)}

      <button
        onClick={() => setPage(p => Math.max(0, p - 1))}
        disabled={page === 0}
      >
        Previous
      </button>

      <button
        onClick={() => setPage(p => p + 1)}
        disabled={isPlaceholderData || !data.hasMore}
      >
        Next
      </button>
    </>
  )
}

Dependent Queries - Sequential data fetching:

function UserProjects({ email }: { email: string }) {
  // First query
  const { data: user } = useQuery({
    queryKey: ['user', email],
    queryFn: () => getUserByEmail(email),
  })

  // Second query waits for first
  const { data: projects } = useQuery({
    queryKey: ['projects', user?.id],
    queryFn: () => getProjectsByUser(user.id),
    enabled: !!user?.id, // Only runs when user.id exists
  })

  return <div>{/* render projects */}</div>
}

Performance Optimization

staleTime is your primary control - adjust this, not gcTime:

// Real-time data (default)
staleTime: 0 // Always considered stale, refetch on mount

// User profiles (changes infrequently)
staleTime: 1000 * 60 * 2 // Fresh for 2 minutes

// Static reference data
staleTime: 1000 * 60 * 10 // Fresh for 10 minutes

Query deduplication happens automatically - multiple components mounting with identical query keys result in a single network request, but all components receive data.

Prevent request waterfalls:

// ❌ Waterfall - each query waits for previous
function Dashboard() {
  const { data: user } = useQuery(userQuery)
  const { data: posts } = useQuery(postsQuery(user?.id))
  const { data: stats } = useQuery(statsQuery(user?.id))
}

// ✅ Parallel - all queries start simultaneously
function Dashboard() {
  const { data: user } = useQuery(userQuery)
  const { data: posts } = useQuery({
    ...postsQuery(user?.id),
    enabled: !!user?.id,
  })
  const { data: stats } = useQuery({
    ...statsQuery(user?.id),
    enabled: !!user?.id,
  })
}

// ✅ Best - prefetch in route loader (see Router × Query Integration)

Never copy server state to local state - this opts out of background updates:

// ❌ Wrong - copies to state, loses reactivity
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const [todos, setTodos] = useState(data)

// ✅ Correct - use query data directly
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

Testing with Mock Service Worker (MSW)

MSW is the recommended approach - mock the network layer:

// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/todos', () => {
    return HttpResponse.json([
      { id: 1, text: 'Test todo', completed: false },
    ])
  }),

  http.post('/api/todos', async ({ request }) => {
    const newTodo = await request.json()
    return HttpResponse.json({ id: 2, ...newTodo })
  }),
]

// src/test/setup.ts
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

export const server = setupServer(...handlers)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Create test wrappers with proper QueryClient:

// src/test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'

export function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // Prevent retries in tests
        gcTime: Infinity,
      },
    },
  })
}

export function renderWithClient(ui: React.ReactElement) {
  const testQueryClient = createTestQueryClient()

  return render(
    <QueryClientProvider client={testQueryClient}>
      {ui}
    </QueryClientProvider>
  )
}

Test queries:

import { renderWithClient } from '@/test/utils'
import { screen } from '@testing-library/react'

test('displays todos', async () => {
  renderWithClient(<TaskList />)

  // Wait for data to load
  expect(await screen.findByText('Test todo')).toBeInTheDocument()
})

test('shows error state', async () => {
  // Override handler for this test
  server.use(
    http.get('/api/todos', () => {
      return HttpResponse.json(
        { message: 'Failed to fetch' },
        { status: 500 }
      )
    })
  )

  renderWithClient(<TaskList />)

  expect(await screen.findByText(/failed/i)).toBeInTheDocument()
})

Critical testing principles:

  • Create new QueryClient per test for isolation
  • Set retry: false to prevent timeouts
  • Use async queries (findBy*) for data that loads
  • Silence console.error for expected errors

Anti-Patterns to Avoid

❌ Don't store query data in Redux/Context:

  • Creates dual sources of truth
  • Loses automatic cache invalidation
  • Triggers unnecessary renders

❌ Don't call refetch() with different parameters:

// ❌ Wrong - breaks declarative pattern
const { data, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: () => fetchTodos(filters),
})
// Later: refetch with different filters??? Won't work!

// ✅ Correct - include params in key
const [filters, setFilters] = useState('all')
const { data } = useQuery({
  queryKey: ['todos', filters],
  queryFn: () => fetchTodos(filters),
})
// Changing filters automatically refetches

❌ Don't use queries for local state:

  • Query Cache expects refetchable data
  • Use useState/useReducer for client-only state

❌ Don't create QueryClient inside components:

// ❌ Wrong - new cache every render
function App() {
  const client = new QueryClient()
  return <QueryClientProvider client={client}>...</QueryClientProvider>
}

// ✅ Correct - stable instance
const queryClient = new QueryClient()
function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

❌ Don't ignore loading and error states - always handle both

❌ Don't transform data by copying to state - use select option

❌ Don't mismatch query keys - be consistent with types ('1' vs 1)

Cache Timing Guidelines

staleTime - How long data is considered fresh:

  • 0 (default) - Always stale, refetch on mount/focus
  • 30_000 (30s) - Good for user-generated content
  • 120_000 (2min) - Good for profile data
  • 600_000 (10min) - Good for static reference data

gcTime (formerly cacheTime) - How long unused data stays in cache:

  • 300_000 (5min, default) - Good for most cases
  • Infinity - Keep forever (useful with persistence)
  • 0 - Immediate garbage collection (not recommended)

Relationship: staleTime controls refetch frequency, gcTime controls memory cleanup.

Related Skills

  • tanstack-router - File-based routing with loader prefetching
  • react-typescript - React 19 patterns, Actions vs Mutations decision guide
  • state-management - Zustand for client state, Query for server state
  • testing-frontend - Testing queries with MSW
  • api-integration - Backend API patterns with Apidog

Router Integration (TanStack Router)

Seamlessly integrate TanStack Router with TanStack Query for optimal SPA performance and instant navigation.

Route Loader + Query Prefetch

The key pattern: Use route loaders to prefetch queries BEFORE navigation completes.

Benefits:

  • Loaders run before render, eliminating waterfall
  • Fast SPA navigations (instant perceived performance)
  • Queries still benefit from cache deduplication
  • Add Router & Query DevTools during development (auto-hide in production)

Basic Pattern

// src/routes/users/$id.tsx
import { createFileRoute } from '@tanstack/react-router'
import { queryClient } from '@/app/queryClient'
import { usersKeys, fetchUser } from '@/features/users/queries'

export const Route = createFileRoute('/users/$id')({
  loader: async ({ params }) => {
    const id = params.id

    return queryClient.ensureQueryData({
      queryKey: usersKeys.detail(id),
      queryFn: () => fetchUser(id),
      staleTime: 30_000, // Fresh for 30 seconds
    })
  },
  component: UserPage,
})

function UserPage() {
  const { id } = Route.useParams()
  const { data: user } = useQuery({
    queryKey: usersKeys.detail(id),
    queryFn: () => fetchUser(id),
  })

  // Data is already loaded from loader, so this returns instantly
  return <div>{user.name}</div>
}

Using Query Options Pattern (Recommended)

Query Options provide maximum type safety and DRY:

// features/users/queries.ts
import { queryOptions } from '@tanstack/react-query'

export function userQueryOptions(userId: string) {
  return queryOptions({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 30_000,
  })
}

export function useUser(userId: string) {
  return useQuery(userQueryOptions(userId))
}

// src/routes/users/$userId.tsx
import { userQueryOptions } from '@/features/users/queries'
import { queryClient } from '@/app/queryClient'

export const Route = createFileRoute('/users/$userId')({
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(params.userId)),
  component: UserPage,
})

function UserPage() {
  const { userId } = Route.useParams()
  const { data: user } = useUser(userId)

  return <div>{user.name}</div>
}

Multiple Queries in Loader

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    // Run in parallel
    await Promise.all([
      queryClient.ensureQueryData(userQueryOptions()),
      queryClient.ensureQueryData(statsQueryOptions()),
      queryClient.ensureQueryData(postsQueryOptions()),
    ])
  },
  component: Dashboard,
})

function Dashboard() {
  const { data: user } = useUser()
  const { data: stats } = useStats()
  const { data: posts } = usePosts()

  // All data pre-loaded, renders instantly
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
      <PostsList posts={posts} />
    </div>
  )
}

Dependent Queries in Loaders

export const Route = createFileRoute('/users/$userId/posts')({
  loader: async ({ params }) => {
    // First ensure user data
    const user = await queryClient.ensureQueryData(
      userQueryOptions(params.userId)
    )

    // Then fetch user's posts
    return queryClient.ensureQueryData(
      userPostsQueryOptions(user.id)
    )
  },
  component: UserPostsPage,
})

Prefetch vs Ensure

prefetchQuery - Fire and forget, don't wait:

loader: ({ params }) => {
  // Don't await - just start fetching
  queryClient.prefetchQuery(userQueryOptions(params.userId))
  // Navigation continues immediately
}

ensureQueryData - Wait for data (recommended):

loader: async ({ params }) => {
  // Await - navigation waits until data is ready
  return await queryClient.ensureQueryData(userQueryOptions(params.userId))
}

fetchQuery - Always fetches fresh:

loader: async ({ params }) => {
  // Ignores cache, always fetches
  return await queryClient.fetchQuery(userQueryOptions(params.userId))
}

Recommendation: Use ensureQueryData for most cases - respects cache and staleTime.

Preloading on Link Hover

import { Link } from '@tanstack/react-router'

<Link
  to="/users/$userId"
  params={{ userId: '123' }}
  preload="intent" // Preload on hover/focus
>
  View User
</Link>

Search Params + Queries

// src/routes/users/index.tsx
import { z } from 'zod'

const searchSchema = z.object({
  page: z.number().default(1),
  filter: z.enum(['active', 'all']).default('all'),
})

export const Route = createFileRoute('/users/')({
  validateSearch: searchSchema,
  loader: ({ search }) => {
    return queryClient.ensureQueryData(
      usersQueryOptions({ page: search.page, filter: search.filter })
    )
  },
  component: UsersPage,
})

function UsersPage() {
  const { page, filter } = Route.useSearch()
  const { data: users } = useUsers({ page, filter })

  return <UsersList users={users} />
}

Related Skills

  • tanstack-router - File-based routing and navigation
  • react-typescript - React component patterns
  • state-management - Client state management (Zustand)
  • testing-frontend - Testing queries with MSW

平台分布

Local Agent

95.42%
按下载量换算649

安全审计

Socket

通过

权限和风险

操作浏览器

该 Skill 可能涉及浏览器控制能力,使用时可能读取或操作网页内容,需要在受控环境中确认权限边界。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills