tanstack-query

📁 blockmatic/basilic 📅 Jan 31, 2026
9
总安装量
8
周安装量
#33389
全站排名
安装命令
npx skills add https://github.com/blockmatic/basilic --skill tanstack-query

Agent 安装分布

opencode 8
cursor 8
claude-code 7
github-copilot 7
codex 7
kimi-cli 7

Skill 文档

Skill: tanstack-query

Scope

  • Applies to: TanStack Query v5+ for async operations, data fetching, caching, mutations, infinite queries, optimistic updates
  • Does NOT cover: URL state management (use nuqs), grouped synchronous state (use ahooks.useSetState), localStorage persistence (use ahooks.useLocalStorageState)

Assumptions

  • TanStack Query v5+
  • React 18+ with hooks support
  • TypeScript v5+ (for type inference)
  • @lukemorales/query-key-factory for query key management
  • QueryClientProvider configured at app root

Principles

  • Use TanStack Query for ANY async operation that benefits from caching or state management
  • Query key factory pattern for centralized, type-safe query keys
  • Never manually manage isLoading, error, or isError states (provided by hooks)
  • queryFn can be any Promise-returning function (not just HTTP calls)
  • All TanStack features work identically regardless of data source: caching, deduping, background refetching, stale-while-revalidate

Constraints

MUST

  • Use query key factory (@lukemorales/query-key-factory) for all query keys
  • Use TanStack Query hooks for async operations (never manually manage loading/error states)
  • Use namespace invalidation: queryClient.invalidateQueries({ queryKey: users._def })

SHOULD

  • Use TanStack Query for any async operation that benefits from caching (HTTP, localStorage reads, local computations, file operations)
  • Extract query logic into custom hooks for reusability
  • Combine multiple queries into cohesive hooks
  • Use TypeScript generics for type-safe queries: useQuery<ResponseType>(...)
  • Configure QueryClient defaults (retry, staleTime) at provider level

AVOID

  • Manually constructing query keys: useQuery({ queryKey: ['users', id] })
  • Hardcoding query keys in invalidations: queryClient.invalidateQueries({ queryKey: ['users'] })
  • Manually managing loading/error states (use hook-provided states)
  • Using for URL-shareable state (use nuqs instead)
  • Using for grouped synchronous state (use ahooks.useSetState instead)
  • Using for one-off promises without caching needs (use plain async/await in event handlers)

Interactions

  • Complements nuqs for URL state management (queries can depend on URL params)
  • Complements ahooks for synchronous state (useSetState for form state, useLocalStorageState for persistence)
  • Works with generated API clients from OpenAPI specs (@repo/react)
  • Part of state management decision tree (see React rules)

Patterns

Query Key Factory Pattern

Centralized, type-safe query keys:

import { createQueryKeys } from '@lukemorales/query-key-factory'

export const users = createQueryKeys('users', {
  detail: (id: string) => ({
    queryKey: [id],
    queryFn: () => fetchUser(id),
  }),
  list: (filters?: Filters) => ({
    queryKey: [filters],
    queryFn: () => fetchUsers(filters),
  }),
})

// Usage
import { useQuery } from '@tanstack/react-query'
import { users } from '@/queries/users'

const { data, isLoading, error } = useQuery(users.detail(userId))

File Organization: src/queries/users.ts – Query key factory for users, src/queries/articles.ts – Query key factory for articles, src/queries/index.ts – Re-export all query keys

Versatile queryFn Pattern

TanStack Query accepts ANY async queryFn—not just HTTP calls:

// Server data fetching (traditional)
const { data } = useQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUser(userId),
})

// Local computation (no network)
const { data } = useQuery({
  queryKey: ['fibonacci', n],
  queryFn: () => computeFibonacci(n),
})

// localStorage read
const { data: settings } = useQuery({
  queryKey: ['user-settings'],
  queryFn: () => Promise.resolve(
    JSON.parse(localStorage.getItem('settings') || '{}')
  ),
})

// File operation
const { data } = useQuery({
  queryKey: ['file-content', path],
  queryFn: () => readFile(path),
})

When to use: Any async operation that benefits from automatic caching, stale management, or repeated execution

When NOT to use: One-off promises without caching needs (use plain async/await in event handlers)

Mutation Pattern

Mutations with automatic invalidation:

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

function useCreateUser() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (data: CreateUserInput) => {
      const response = await createUser(data)
      return response
    },
    onSuccess: () => {
      // Namespace invalidation (recommended)
      queryClient.invalidateQueries({ queryKey: users._def })
    },
  })
}

// Usage
const createUser = useCreateUser()
createUser.mutate({ name: 'John', email: 'john@example.com' })

Infinite Query Pattern

Pagination with infinite scroll:

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

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['users', 'infinite'],
  queryFn: ({ pageParam = 0 }) => fetchUsers({ page: pageParam }),
  getNextPageParam: (lastPage, pages) => lastPage.nextPage,
  initialPageParam: 0,
})

QueryClient Configuration

Configure defaults at provider level:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

function Providers({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1 minute
        retry: 1,
        refetchOnWindowFocus: false,
      },
    },
  }))

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

Generated API Client Integration

Use generated hooks from OpenAPI specs:

import { useHealthCheck } from '@repo/react'

function HealthStatus() {
  const { data, isLoading, error } = useHealthCheck()
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  
  return <div>Status: {data.status}</div>
}

Custom Hook Pattern

Extract query logic into reusable hooks:

// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query'
import { users } from '@/queries/users'

export function useUser(userId: string) {
  return useQuery(users.detail(userId))
}

// Component usage
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useUser(userId)
  // ...
}

State Management Decision Tree

  1. URL-shareable state → Use nuqs (filters, search, tabs, pagination)
  2. Grouped state not in URL → Use ahooks.useSetState (form state, game engine, ephemeral UI)
  3. Async operations → Use TanStack Query (data fetching, mutations, caching)
  4. localStorage persistence → Use ahooks.useLocalStorageState (preferences, settings)
  5. Simple independent state → Use useState (rare, prefer other options)

Use TanStack Query for: Server data fetching, mutations, optimistic updates, background refetching, caching, any async operation that benefits from state management

Don’t use TanStack Query for: URL-shareable state (use nuqs), grouped synchronous state (use ahooks.useSetState), one-off promises without caching needs

References