Agent skill

Tanstack Query

by bskimball in bskimball/tanstack-hono

Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.

MITAuto-check passedFrontend & Design

Install Tanstack Query

skills CLI
$ npx skills add bskimball/tanstack-hono --skill tanstack-query -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install bskimball/tanstack-hono tanstack-query --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/bskimball/tanstack-hono.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/tanstack-query .claude/skills/tanstack-query && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
tanstack-query
GitHub stars
119
Used in
1 other repo
Token cost
~5.4k tokens
SKILL.md length
374 words
Files
1
Skills in repo
3
Repo updated
First seen
Licence
MIT

At a glance

Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.

  • Works in 12 steps: Use queryOptions helper for type-safe,… → Structure query keys hierarchically for… → Set appropriate staleTime - 0 means… → …
  • Tasks that involve Async programming
  • SKILL.md covers Overview, Installation, Setup and Core Concepts, plus 14 more sections
  • Calls npm

What it does

Tanstack Query is an agent skill from bskimball/tanstack-hono. Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.

Its SKILL.md is about 5.4k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Frontend & Design, covering Async programming and State management. It works with TanStack, Angular, React and Svelte. The repository describes itself as: A simple example of using Tanstack Router SSR with Hono in a monolith. The licence is MIT.

When your agent uses it

  • Tasks that involve Async programming
  • Tasks that involve State management

Example prompts

  • “/tanstack-query”

Requirements

  • Node.js

Workflow steps

12 steps, taken from the first numbered list in SKILL.md.

  1. Use queryOptions helper for type-safe, reusable query configurations
  2. Structure query keys hierarchically for granular invalidation
  3. Set appropriate staleTime - 0 means always refetch on mount (default), increase for less dynamic data
  4. Use placeholderData (not initialData) for keeping previous page data during pagination
  5. Prefer useSuspenseQuery when using Suspense boundaries for cleaner component code
  6. Use enabled for dependent queries, not conditional hook calls
  7. Always invalidate after mutations - don't rely solely on optimistic updates
  8. Cancel queries in onMutate before optimistic updates to prevent race conditions
  9. Use ensureQueryData in route loaders instead of prefetchQuery for immediate access
  10. Set retry: false in tests to avoid timeout issues
  11. Don't destructure the query result if you need to pass it around (breaks reactivity)
  12. Use select for derived data instead of transforming in the component

What it can do on your machine

Read from SKILL.md and the folder at commit 2a7f052. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • npm

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use npm, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Tanstack Query loads about 5.4k tokens when it runs. Until then it costs about 36 tokens; SKILL.md has 374 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~36
When it runs · the whole SKILL.md, loaded when a task matches
~5.4k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from bskimball/tanstack-hono at commit 2a7f052, republished under its MIT licence (© bskimball). 374 words, ~5,425 tokens.

Download SKILL.mdSave it as .claude/skills/tanstack-query/SKILL.md (or your agent's skills folder).
name
tanstack-query
description
Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.

Overview

TanStack Query (formerly React Query) manages server state - data that lives on the server and needs to be fetched, cached, synchronized, and updated. It provides automatic caching, background refetching, stale-while-revalidate patterns, pagination, infinite scrolling, and optimistic updates out of the box.

Package: @tanstack/react-query Devtools: @tanstack/react-query-devtools Current Version: v5

Installation

bash
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools  # Optional

Setup

tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      gcTime: 1000 * 60 * 5, // 5 minutes (garbage collection)
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

Core Concepts

Query Keys

Query keys uniquely identify cached data. They must be serializable arrays:

tsx
// Simple key
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// With variables (dependency array pattern)
useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodos })

// Hierarchical keys for invalidation
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
useQuery({ queryKey: ['todos', todoId, 'comments'], queryFn: () => fetchComments(todoId) })

// Invalidation matches prefixes:
// queryClient.invalidateQueries({ queryKey: ['todos'] })
// ^ Invalidates ALL queries starting with 'todos'
Query Functions
tsx
// Query function receives a QueryFunctionContext
useQuery({
  queryKey: ['todos', todoId],
  queryFn: async ({ queryKey, signal, meta }) => {
    const [_key, id] = queryKey
    const response = await fetch(`/api/todos/${id}`, { signal })
    if (!response.ok) throw new Error('Failed to fetch')
    return response.json()
  },
})

// Using the signal for automatic cancellation
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const response = await fetch('/api/todos', { signal })
    return response.json()
  },
})
queryOptions Helper

Create reusable, type-safe query configurations:

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

export const todosQueryOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoQueryOptions = (todoId: string) =>
  queryOptions({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodo(todoId),
    enabled: !!todoId,
  })

// Usage
const { data } = useQuery(todosQueryOptions)
const { data } = useSuspenseQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todosQueryOptions)

Queries (useQuery)

Basic Usage
tsx
import { useQuery } from '@tanstack/react-query'

function Todos() {
  const {
    data,
    error,
    isLoading,      // First load, no data yet
    isFetching,     // Any fetch in progress (including background)
    isError,
    isSuccess,
    isPending,      // No data yet (same as isLoading in most cases)
    status,         // 'pending' | 'error' | 'success'
    fetchStatus,    // 'fetching' | 'paused' | 'idle'
    refetch,
    isStale,
    isPlaceholderData,
    dataUpdatedAt,
    errorUpdatedAt,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error.message} />
  return <TodoList todos={data} />
}
Query Options
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,

  // Freshness
  staleTime: 5000,            // ms data stays fresh (default: 0)
  gcTime: 300000,             // ms unused data stays in cache (default: 5 min)

  // Refetching
  refetchInterval: 10000,     // Poll every 10s
  refetchIntervalInBackground: false, // Don't poll when tab hidden
  refetchOnMount: true,       // Refetch on component mount if stale
  refetchOnWindowFocus: true, // Refetch on window focus if stale
  refetchOnReconnect: true,   // Refetch on network reconnect

  // Retry
  retry: 3,                   // Number of retries (or function)
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),

  // Conditional
  enabled: !!userId,          // Only run when truthy

  // Initial/placeholder data
  initialData: () => cachedData,
  initialDataUpdatedAt: Date.now() - 10000,
  placeholderData: (previousData) => previousData, // keepPreviousData pattern
  placeholderData: initialTodos,

  // Transform
  select: (data) => data.filter(todo => !todo.done),

  // Structural sharing (default: true)
  structuralSharing: true,

  // Network mode
  networkMode: 'online', // 'online' | 'always' | 'offlineFirst'

  // Meta (accessible in query function context)
  meta: { purpose: 'user-facing' },
})

Mutations (useMutation)

Basic Usage
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddTodo() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newTodo: { title: string }) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(res => res.json())
    },
    // Lifecycle callbacks
    onMutate: async (variables) => {
      // Called before mutationFn
      // Good for optimistic updates
      return { previousTodos } // context for onError
    },
    onSuccess: (data, variables, context) => {
      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
    onError: (error, variables, context) => {
      // Rollback optimistic updates
      queryClient.setQueryData(['todos'], context.previousTodos)
    },
    onSettled: (data, error, variables, context) => {
      // Always runs (success or error)
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Todo' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Adding...' : 'Add Todo'}
    </button>
  )
}
Mutation State
tsx
const {
  mutate,         // Fire-and-forget
  mutateAsync,    // Returns promise
  isPending,      // Mutation in progress
  isError,
  isSuccess,
  isIdle,         // Not yet fired
  data,           // Success response
  error,          // Error object
  reset,          // Reset state to idle
  variables,      // Variables passed to mutate
  status,         // 'idle' | 'pending' | 'error' | 'success'
} = useMutation({ ... })

Optimistic Updates

tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 1. Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 2. Snapshot previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 3. Optimistically update
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 4. Return context for rollback
    return { previousTodo }
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
  },
  onSettled: () => {
    // Always refetch to sync with server
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
Optimistic Updates on Lists
tsx
onMutate: async (newTodo) => {
  await queryClient.cancelQueries({ queryKey: ['todos'] })
  const previousTodos = queryClient.getQueryData(['todos'])

  queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

  return { previousTodos }
},
onError: (err, newTodo, context) => {
  queryClient.setQueryData(['todos'], context.previousTodos)
},

Query Invalidation

tsx
const queryClient = useQueryClient()

// Invalidate all queries
queryClient.invalidateQueries()

// Invalidate by prefix
queryClient.invalidateQueries({ queryKey: ['todos'] })

// Invalidate exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true })

// Invalidate with predicate
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1]?.status === 'done',
})

// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'] })

// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] })

// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })

Infinite Queries

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

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    fetchPreviousPage,
    hasNextPage,
    hasPreviousPage,
    isFetchingNextPage,
    isFetchingPreviousPage,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: async ({ pageParam }) => {
      const res = await fetch(`/api/projects?cursor=${pageParam}`)
      return res.json()
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages, lastPageParam) => {
      return lastPage.nextCursor ?? undefined // undefined = no more pages
    },
    getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
      return firstPage.prevCursor ?? undefined
    },
    maxPages: 3, // Keep max 3 pages in cache (for performance)
  })

  return (
    <div>
      {data.pages.map((page) =>
        page.items.map((item) => <Item key={item.id} item={item} />)
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
      </button>
    </div>
  )
}

Parallel Queries

tsx
// Multiple independent queries run in parallel automatically
function Dashboard() {
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })

  // Both fetch simultaneously
}

// Dynamic parallel queries with useQueries
function UserProjects({ userIds }) {
  const queries = 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),
    }),
  })
}

Dependent Queries

tsx
// Sequential queries using enabled
function UserPosts({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPostsByUser(userId),
    enabled: !!userQuery.data, // Only run when user is loaded
  })
}

Paginated Queries

tsx
function PaginatedList() {
  const [page, setPage] = useState(1)

  const { data, isPlaceholderData } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
    placeholderData: (previousData) => previousData, // Keep showing old data
  })

  return (
    <div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
      {data.items.map(item => <Item key={item.id} item={item} />)}
      <button
        onClick={() => setPage(p => p + 1)}
        disabled={isPlaceholderData || !data.hasMore}
      >
        Next
      </button>
    </div>
  )
}

Suspense Integration

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

// Component will suspend until data is loaded
function TodoList() {
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
  // data is guaranteed to be defined here
  return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}

// Wrap with Suspense boundary
function App() {
  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <TodoList />
      </Suspense>
    </ErrorBoundary>
  )
}

// Multiple suspense queries (fetch in parallel)
function Dashboard() {
  const [{ data: users }, { data: projects }] = useSuspenseQueries({
    queries: [
      { queryKey: ['users'], queryFn: fetchUsers },
      { queryKey: ['projects'], queryFn: fetchProjects },
    ],
  })
}

Prefetching

tsx
const queryClient = useQueryClient()

// Prefetch on hover
function TodoLink({ todoId }) {
  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['todo', todoId],
      queryFn: () => fetchTodo(todoId),
      staleTime: 5000, // Only prefetch if data older than 5s
    })
  }

  return (
    <Link to={`/todos/${todoId}`} onMouseEnter={prefetch}>
      Todo {todoId}
    </Link>
  )
}

// Prefetch in route loader (TanStack Router integration)
export const Route = createFileRoute('/todos/$todoId')({
  loader: ({ context: { queryClient }, params: { todoId } }) =>
    queryClient.ensureQueryData(todoQueryOptions(todoId)),
})

// Prefetch infinite queries
queryClient.prefetchInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  pages: 3, // Prefetch first 3 pages
})

SSR & Hydration

Server-Side Prefetching
tsx
// Server component or loader
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'

async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Page({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Todos />
    </HydrationBoundary>
  )
}
Streaming SSR (React Server Components)
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { makeQueryClient } from './query-client'

export default async function Page() {
  const queryClient = makeQueryClient()

  // Prefetch on server
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

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

QueryClient API

tsx
const queryClient = useQueryClient()

// Get cached data
queryClient.getQueryData(['todos'])

// Set cached data
queryClient.setQueryData(['todos'], updatedTodos)
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

// Get query state
queryClient.getQueryState(['todos'])

// Check if fetching
queryClient.isFetching({ queryKey: ['todos'] })
queryClient.isMutating()

// Cancel queries
queryClient.cancelQueries({ queryKey: ['todos'] })

// Invalidate (marks stale, refetches active)
queryClient.invalidateQueries({ queryKey: ['todos'] })

// Refetch (force refetch even if fresh)
queryClient.refetchQueries({ queryKey: ['todos'] })

// Remove from cache
queryClient.removeQueries({ queryKey: ['todos'] })

// Reset to initial state
queryClient.resetQueries({ queryKey: ['todos'] })

// Clear entire cache
queryClient.clear()

// Prefetch
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.ensureQueryData({ queryKey: ['todos'], queryFn: fetchTodos })

// Get/set defaults
queryClient.setQueryDefaults(['todos'], { staleTime: 10000 })
queryClient.getQueryDefaults(['todos'])
queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo })

Testing

tsx
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // Don't retry in tests
        gcTime: Infinity, // Prevent garbage collection during tests
      },
    },
  })
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test('fetches todos', async () => {
  const { result } = renderHook(() => useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  }), { wrapper: createWrapper() })

  await waitFor(() => expect(result.current.isSuccess).toBe(true))
  expect(result.current.data).toEqual(expectedTodos)
})

// Mock with setQueryData for component tests
test('renders todos', () => {
  const queryClient = new QueryClient()
  queryClient.setQueryData(['todos'], mockTodos)

  render(
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  )

  expect(screen.getByText('Todo 1')).toBeInTheDocument()
})

TypeScript Patterns

Typing Query Functions
tsx
interface Todo {
  id: number
  title: string
  completed: boolean
}

// Type is inferred from queryFn return type
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})
// data: Todo[] | undefined

// With select
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data): string[] => data.map(t => t.title),
})
// data: string[] | undefined
Typing Errors
tsx
// Default error type is Error
const { error } = useQuery<Todo[], AxiosError>({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

// Or register globally
declare module '@tanstack/react-query' {
  interface Register {
    defaultError: AxiosError
  }
}
tsx
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'

export const todosOptions = queryOptions({
  queryKey: ['todos'] as const,
  queryFn: fetchTodos,
  staleTime: 5000,
})

export const todoOptions = (id: string) =>
  queryOptions({
    queryKey: ['todos', id] as const,
    queryFn: () => fetchTodo(id),
    enabled: !!id,
  })

// Full type inference everywhere
const { data } = useQuery(todosOptions)
const { data } = useSuspenseQuery(todoOptions('123'))
await queryClient.ensureQueryData(todosOptions)
queryClient.invalidateQueries({ queryKey: todosOptions.queryKey })

Advanced Patterns

Window Focus Refetching
tsx
// Disable globally
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { refetchOnWindowFocus: false },
  },
})

// Custom focus manager
import { focusManager } from '@tanstack/react-query'

// For React Native
focusManager.setEventListener((handleFocus) => {
  const subscription = AppState.addEventListener('change', (state) => {
    handleFocus(state === 'active')
  })
  return () => subscription.remove()
})
Network Mode
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  // 'online' (default): only fetch when online
  // 'always': always fetch (useful for local-first)
  // 'offlineFirst': try fetch, use cache if offline
  networkMode: 'offlineFirst',
})
Query Cancellation
tsx
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    // signal is AbortSignal - automatically cancelled on unmount or key change
    const res = await fetch('/api/todos', { signal })
    return res.json()
  },
})

// Manual cancellation
queryClient.cancelQueries({ queryKey: ['todos'] })
Persistence
tsx
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24, // 24 hours
})

Best Practices

  1. Use queryOptions helper for type-safe, reusable query configurations
  2. Structure query keys hierarchically for granular invalidation
  3. Set appropriate staleTime - 0 means always refetch on mount (default), increase for less dynamic data
  4. Use placeholderData (not initialData) for keeping previous page data during pagination
  5. Prefer useSuspenseQuery when using Suspense boundaries for cleaner component code
  6. Use enabled for dependent queries, not conditional hook calls
  7. Always invalidate after mutations - don't rely solely on optimistic updates
  8. Cancel queries in onMutate before optimistic updates to prevent race conditions
  9. Use ensureQueryData in route loaders instead of prefetchQuery for immediate access
  10. Set retry: false in tests to avoid timeout issues
  11. Don't destructure the query result if you need to pass it around (breaks reactivity)
  12. Use select for derived data instead of transforming in the component
  13. Keep query functions pure - they should only fetch, not cause side effects
  14. Use gcTime: Infinity in tests to prevent cache cleanup during assertions
Show full SKILL.md (70 more words)Show less

Common Pitfalls

  • Using initialData when you mean placeholderData (initialData counts as "fresh" data)
  • Not providing initialPageParam for infinite queries (required in v5)
  • Calling hooks conditionally (violates React rules)
  • Not cancelling queries before optimistic updates (race conditions)
  • Setting staleTime higher than gcTime (data gets garbage collected while "fresh")
  • Forgetting to wrap tests with QueryClientProvider
  • Using same QueryClient instance across tests (shared state)
  • Not awaiting invalidateQueries in mutation callbacks when order matters

© bskimball, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .agents/skills/tanstack-query of bskimball/tanstack-hono.

Open the folder on GitHubat commit 2a7f052

Used in 1 other repository

We found 1 copy of this SKILL.md (exact, near-identical or edited) in other folders, from 1 other GitHub owner. This page covers the copy in bskimball/tanstack-hono, which our catalogue first saw on October 7, 2026.

Compare with similar skills

Tanstack Query next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Tanstack Query compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Tanstack Query this skillbskimball/tanstack-hono1191 repos~5.4kAutomated safety check: PassMIT
Olore Tanstack Store Latestolorehq/olore104—~1.2kAutomated safety check: PassMIT
Tanstack Virtualudecode/plate17k—~2.8kAutomated safety check: PassCustom licence
Lunora Realtimeanolilab/lunora283—~2.7kAutomated safety check: PassCustom licence
Boneyard0xGF/boneyard7.5k—~2.2kAutomated safety check: NotesMIT
Markstream InstallSimon-He95/markstream-vue3k—~2.2kAutomated safety check: PassMIT

Similar skills

  • Local tanstack-store documentation reference (latest). An agent skill from olorehq/olore.

    104 GitHub stars~1.2k tokensUpdated today
    Frontend & DesignAuto-check passed
  • Tanstack Virtual

    udecode/plate

    Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular.

    17k GitHub stars~2.8k tokensUpdated today
    Frontend & DesignAuto-check passed
  • Lunora Realtime

    anolilab/lunora

    Wires Lunora's live data into a client app. An agent skill from anolilab/lunora.

    283 GitHub stars~2.7k tokensUpdated today
    MobileAuto-check passed
  • Boneyard

    0xGF/boneyard

    Use boneyard-js to add, configure, debug, or rebuild skeleton screens.

    7.5k GitHub stars~2.2k tokensUpdated 1 mo ago
    Frontend & DesignAuto-check: notes
  • Markstream Install

    Simon-He95/markstream-vue

    Install and wire markstream-vue, markstream-react, markstream-vue2, markstream-angular, or markstream-svelte into an existing repository.

    3k GitHub stars~2.2k tokensUpdated 3 days ago
    Frontend & DesignAuto-check passed
  • Solar Icons

    saoudi-h/solar-icons

    Add Solar Icons via @solar-icons/cli to any React, Vue, Svelte, Solid, Angular, React Native, Nuxt, Static, vanilla JS, or Laravel Blade project.

    184 GitHub stars~2.1k tokensUpdated today
    Frontend & DesignAuto-check passed

More from bskimball/tanstack-hono

  • Hono

    bskimball/tanstack-hono

    A skill your agent uses when building Hono web applications or when the user asks about Hono APIs, routing, middleware, JSX, validation, testing, or streaming.

    119 GitHub stars~3.4k tokensUpdated 27 days ago
    Auto-check passed
  • Tanstack Router

    bskimball/tanstack-hono

    Type-safe routing for React and Solid applications with first-class search params, data loading, and seamless integration with the React ecosystem.

    119 GitHub stars~4.4k tokensUpdated 27 days ago
    Auto-check passed

Questions about Tanstack Query

What does Tanstack Query do?

Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular. Tanstack Query is an agent skill from bskimball/tanstack-hono. Powerful asynchronous state management, server-state utilities, and data fetching for TS/JS, React, Vue, Solid, Svelte & Angular.

When should I use Tanstack Query?

Tanstack Query fits situations like: tasks that involve Async programming; tasks that involve State management.

How do I install Tanstack Query in Claude Code?

Run `npx skills add bskimball/tanstack-hono --skill tanstack-query -a claude-code`. Or copy the skill folder (.agents/skills/tanstack-query in bskimball/tanstack-hono) into .claude/skills/tanstack-query in your project. Claude Code loads it when a task matches its description.

How do I install Tanstack Query in Codex?

Run `npx skills add bskimball/tanstack-hono --skill tanstack-query -a codex`. Or copy the skill folder (.agents/skills/tanstack-query in bskimball/tanstack-hono) into .agents/skills/tanstack-query in your project. Codex loads it when a task matches its description.

Can I use Tanstack Query in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add bskimball/tanstack-hono --skill tanstack-query -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/tanstack-query, .gemini/skills/tanstack-query, .github/skills/tanstack-query and .opencode/skills/tanstack-query in your project.

What does Tanstack Query need to run?

Going by SKILL.md and its folder, Tanstack Query needs the command-line tools its instructions call (npm). Our summary lists: Node.js.

Does Tanstack Query access the network?

SKILL.md contains no URLs. Its commands use npm, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Tanstack Query safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Tanstack Query use?

Tanstack Query is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Tanstack Query use?

About 5.4k tokens (SKILL.md is roughly 22k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Tanstack Query?

Skills that share tags, products or a category with Tanstack Query: Olore Tanstack Store Latest (olorehq/olore, 104 stars), Tanstack Virtual (udecode/plate, 17k stars), Lunora Realtime (anolilab/lunora, 283 stars) and Boneyard (0xGF/boneyard, 7.5k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Tanstack Query?

bskimball (a GitHub user) maintains it in bskimball/tanstack-hono, which has 119 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on September 11, 2026.

Source: bskimball/tanstack-hono on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.