tanstack-query
npx skills add https://github.com/blockmatic/basilic --skill tanstack-query
Agent 安装分布
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-factoryfor 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, orisErrorstates (provided by hooks) queryFncan 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
- URL-shareable state â Use
nuqs(filters, search, tabs, pagination) - Grouped state not in URL â Use
ahooks.useSetState(form state, game engine, ephemeral UI) - Async operations â Use TanStack Query (data fetching, mutations, caching)
- localStorage persistence â Use
ahooks.useLocalStorageState(preferences, settings) - 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
- TanStack Query documentation – Official documentation
- Query Key Factory – Query key factory library
- React rules – State management decision tree and patterns
- ahooks – Utility hooks for synchronous state
- OpenAPI Integration – Generated client patterns