SuperAPI AI Directory

tanstack-query-v5

Coding & development

View repository

Updated 10-15 14:20

About this 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)
  • Handwritten query-key tuples in shared query modules (query-key-factory is optional)
  • QueryClientProvider configured at app root

Principles

  • Use TanStack Query for ANY async operation that benefits from caching or state management
  • Prefer handwritten query-key tuples colocated with hooks or in a shared queries/ module
  • 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 stable, hierarchical query keys (handwritten tuples or optional query-key-factory)
  • Use TanStack Query hooks for async operations (never manually manage loading/error states)

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
  • Use @lukemorales/query-key-factory when the project already standardizes on it

AVOID

  • Hardcoding unrelated query keys in invalidations without a shared prefix
  • 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-v2 for URL state management (queries can depend on URL params)
  • Complements ahooks-v3 for synchronous state (useSetState for form state, useLocalStorageState for persistence)
  • Works with OpenAPI-generated API clients; React Query hooks are handwritten against those clients
  • Part of state management decision tree (see React rules)

Patterns

Handwritten Query Keys

Colocated, stable query keys:

export const userKeys = {
  all: ['users'] as const,
  detail: (id: string) => [...userKeys.all, id] as const,
  list: (filters?: Filters) => [...userKeys.all, 'list', filters] as const,
}

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

const { data, isLoading, error } = useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => fetchUser(userId),
})

Optional Query Key Factory

When the project already uses @lukemorales/query-key-factory:

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

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

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 { userKeys } from '@/queries/users'

function useCreateUser() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (data: CreateUserInput) => {
      const response = await createUser(data)
      return response
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.all })
    },
  })
}

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

Handwritten Hook Pattern

Wrap the generated OpenAPI client in custom hooks:

import { useQuery } from '@tanstack/react-query'
import { apiClient } from '@/lib/api-client'

export function useHealthCheck() {
  return useQuery({
    queryKey: ['health'],
    queryFn: () => apiClient.GET('/health').then(r => r.data),
  })
}

Custom Hook Pattern

Extract query logic into reusable hooks:

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

export function useUser(userId: string) {
  return useQuery({
    queryKey: userKeys.detail(userId),
    queryFn: () => fetchUser(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