agentleFS
FirzusFirzus/agent-skills/skills/web/tanstack-query/SKILL.md

tanstack-query

Skill4 starsChanged 20 days agoMITView on GitHub

TanStack Query v5 (React Query) async state management for React. Use when implementing or reviewing code that uses `@tanstack/react-query` — query keys, queryOptions, useQuery/useSuspenseQuery, mutations and invalidation, QueryClient defaults, SSR hydration, or infinite queries.

19 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use tanstack-form, tanstack-table and 1 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • heryTzSkill

    tanstack-form

    Create or modify forms using TanStack Form's composition API. Uses formOptions() as the form shape primitive, withForm HOC for decomposing large forms into sections, withFieldGroup for reusable field groups across forms, Zod for validation, and i18n-prefixed error messages. Use when creating or modifying forms in the web app.

    Also in 3 projects that use this

  • JasonPaffSkill

    tanstack-table

    Build headless data tables with TanStack Table v8. Server-side pagination, filtering, sorting, and virtualization for Cloudflare Workers + D1. Prevents 12 documented errors.

    Also in 3 projects that use this

  • SentrySkill

    review

    Fixture review skill.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

5 rules. 0 are common, 3 are distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 18 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If the app server-renders, identify the prefetch boundary before writing any hook — see ssr.md.

    line 44
  2. Very specific:

    Never retry blindly on 4xx: a retry predicate that inspects the status avoids three doomed round-trips on every 404.

    line 161
  3. Very specific:

    Call hooks unconditionally.

    line 188
  4. Distinctive:

    Put the page in the key and keep the previous page on screen while the next one loads:

    line 257
  5. Distinctive:

    Every query key is an array that contains all variables shaping the response, ordered generic-to-specific so prefix invalidation works.

    line 279

“Common” is measured: kept by at least 6 in 10 of the 18 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

TanStack Query

Reference for TanStack Query v5 (@tanstack/react-query), the async state manager for server data in React. Prefer the project's existing API client, query-key conventions, and hook layer; apply these rules on top so cache identity, staleness, and invalidation stay predictable.

TanStack Query owns server state: data it did not create, stored elsewhere, potentially stale at any moment. It is not a replacement for client state (form inputs, modal toggles, wizard steps) — keep those in React state.

Branch-specific references, loaded on demand:

  • ssr.md — prefetch, dehydrate, HydrationBoundary, Next.js App Router and Server Components.
  • mutations.md — useMutation lifecycle, optimistic updates, rollback, invalidation strategy.
  • infinite-queries.md — useInfiniteQuery, cursors, bi-directional lists, page-limited caches.
  • typescript.md — inference, narrowing, global Register types, skipToken.

First Checks

  1. Confirm @tanstack/react-query is installed and on v5 (package.json, lockfile). v4 code differs on renamed options — cacheTime became gcTime, useErrorBoundary became throwOnError, keepPreviousData became placeholderData: keepPreviousData.
  2. Find where the QueryClient is created and which defaultOptions are set — staleTime, gcTime, retry change the behavior of every hook below.
  3. Find the existing query-key convention (flat arrays, key factories, or queryOptions functions) and reuse it instead of inventing a new one.
  4. Find the existing fetch layer: query functions must throw on failure, and fetch does not throw on a 4xx/5xx by itself.
  5. If the app server-renders, identify the prefetch boundary before writing any hook — see ssr.md.

Keys: The Key Is The Cache Identity

A query key is an array, serializable with JSON.stringify, and unique to the data it describes. Every variable the query function reads and that changes the response belongs in the key. Keys act as dependencies: when the key changes, the query is a different cache entry and refetches.

CasePatternWhy
Generic listqueryKey: ['todos']Constant key for a non-hierarchical resource.
Item by idqueryKey: ['todo', 5]Primitive identifies the item.
Extra parametersqueryKey: ['todos', { status, page }]Object holds the parameters that shape the response.
ScopequeryKey: ['todos', tenantId, { status }]Scope-changing inputs are part of the identity.

Hashing rules that decide whether two keys are the same entry:

  • Object keys are hashed deterministically: { status, page } and { page, status } are the same key, and a property set to undefined is ignored.
  • Array item order matters: ['todos', status, page] and ['todos', page, status] are two different entries.
  • Prefix matching drives invalidation: ['todos'] matches ['todos', { page: 1 }] unless exact: true is passed. Order keys from generic to specific so the prefix you want to invalidate is the leftmost segment.

Colocate With queryOptions

queryOptions returns its input unchanged at runtime, but it ties queryKey and queryFn together and carries the result type into every consumer. Prefer it over loose key constants as soon as a query is used in more than one place.

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

export function todoOptions(todoId: string) {
  return queryOptions({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodoById(todoId),
    staleTime: 5 * 60 * 1000,
  })
}

useQuery(todoOptions('5'))
useSuspenseQuery(todoOptions('5'))
queryClient.prefetchQuery(todoOptions('5'))
queryClient.setQueryData(todoOptions('5').queryKey, nextTodo)
  • useQueries({ queries: [todoOptions('1'), todoOptions('2')] }) runs them in parallel from the same definition.
  • Override per component with a spread: useQuery({ ...todoOptions(id), select }).
  • infiniteQueryOptions is the equivalent helper for infinite queries; mutationOptions for mutations.
  • queryClient.getQueryData(todoOptions(id).queryKey) is typed thanks to the helper — without it the result is unknown (typescript.md).

Query Functions

A query function returns a promise that resolves the data or throws.

useQuery({
  queryKey: ['todos', todoId],
  queryFn: async ({ signal }) => {
    const response = await fetch(`/api/todos/${todoId}`, { signal })

    if (!response.ok) {
      throw new Error('Network response was not ok')
    }

    return response.json()
  },
})
  • Resolving undefined is treated as a failed query. Resolve null to store "nothing" as a success.
  • fetch does not throw on HTTP error statuses — throw explicitly.
  • The function receives a QueryFunctionContext: queryKey, client, signal (pass it through for cancellation), and meta.

Defaults That Decide Behavior

v5 defaults, worth knowing because they explain most surprises:

OptionDefaultEffect
staleTime0Cached data is stale immediately, so it refetches on mount, window focus, and reconnect.
gcTime5 * 60 * 1000Inactive entries (no mounted observer) are garbage collected after 5 minutes.
retry3Failures retry three times with exponential backoff before surfacing an error.
structuralSharingtrueUnchanged parts of the response keep their reference, so consumers do not re-render.
mutation retry0Mutations do not retry by default.
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,
      retry: (failureCount, error) =>
        error instanceof HttpError && error.status < 500 ? false : failureCount < 3,
    },
  },
})
  • staleTime is the main lever against excessive refetching; tune it per endpoint (via queryOptions) rather than disabling refetchOnWindowFocus globally.
  • staleTime: Infinity stops staleness-based refetching but still responds to invalidateQueries. staleTime: 'static' also ignores manual invalidation and refetchOn*: 'always' — reserve it for data that cannot change while the app runs (boot-time feature flags, permissions loaded at login).
  • gcTime is a cache-retention timer, not a freshness timer; it only starts once a query has no observers.
  • Never retry blindly on 4xx: a retry predicate that inspects the status avoids three doomed round-trips on every 404.

Reading A Query

const { data, error, status, isPending, isFetching, isError } = useQuery(
  todoOptions(todoId),
)
  • status: 'pending' | 'error' | 'success' describes the data; fetchStatus: 'fetching' | 'paused' | 'idle' describes the request. A query with cached data that is refetching is success + fetching.
  • isPending means "no data yet". Use isFetching for background-refresh indicators over already-rendered data.
  • For disabled or lazy queries use isLoading (isPending && isFetching) — a disabled query is pending forever and would pin a spinner on screen.
  • select subscribes the component to a slice of the data and re-runs only when data or the function reference changes; extract it to a stable reference or wrap it in useCallback instead of inlining.
  • Re-renders are tracked per accessed property via a Proxy. Object rest destructuring (const { data, ...rest }) touches every property and disables that optimization.

Disabling And Dependent Queries

Call hooks unconditionally; put the condition in enabled or in the query function.

const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: () => getUserByEmail(email),
})

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => getProjectsByUser(user!.id),
  enabled: !!user?.id,
})
  • A disabled query ignores invalidateQueries and refetchQueries, does not fetch on mount, and does not refetch in the background.
  • skipToken as the queryFn is the type-safe alternative to enabled: false and removes the non-null assertion — but refetch() then fails with Missing queryFn. Use enabled: false when manual refetch() is required.
  • Dependent queries are a client-side waterfall. When both are needed for the first paint, prefetch them on the server (ssr.md).

Suspense

const { data } = useSuspenseQuery(todoOptions(todoId))
  • data is guaranteed defined, so no status handling is needed; loading goes to <Suspense> and errors to an error boundary.
  • The trade-off: no enabled, no placeholderData, and queries inside one component fetch in serial. Use useSuspenseQueries to parallelize.
  • Errors are only thrown to the boundary when there is no data to show (throwOnError defaults to (error, query) => typeof query.state.data === 'undefined'). Throw manually if every error must reach the boundary.
  • Reset errors on retry with QueryErrorResetBoundary or useQueryErrorResetBoundary, wired to the error boundary's onReset.
  • Wrap key changes in startTransition so the fallback does not replace the rendered UI on every update.
  • With SSR, only use useSuspenseQuery for queries that are always prefetched — a forgotten prefetch produces a hydration mismatch.

Mutations And Invalidation

const queryClient = useQueryClient()

const { mutate, isPending } = useMutation({
  mutationFn: addTodo,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
  • invalidateQueries marks matching queries stale — overriding their staleTime — and refetches the ones currently rendered.
  • Match by prefix by default; add exact: true to hit a single entry, or predicate for anything finer.
  • Return the promise from onSuccess/onSettled to keep the mutation isPending until the refetch lands.
  • mutate is fire-and-forget with callbacks; mutateAsync returns a promise you must catch yourself.

Optimistic updates, rollback, useMutationState, and offline behavior are in mutations.md.

Pagination

Put the page in the key and keep the previous page on screen while the next one loads:

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

const { data, isPlaceholderData } = useQuery({
  queryKey: ['projects', page],
  queryFn: () => fetchProjects(page),
  placeholderData: keepPreviousData,
})

Without it the UI flips between pending and success on every page change. Guard the "next" control with isPlaceholderData so a page is not skipped. placeholderData is never written to the cache; initialData is.

For "load more" and infinite scroll, see infinite-queries.md.

Review Checklist

  • Every query key is an array that contains all variables shaping the response, ordered generic-to-specific so prefix invalidation works.
  • Shared queries go through a queryOptions function rather than duplicated key/fn pairs.
  • Query functions throw on HTTP errors and never resolve undefined.
  • staleTime is set deliberately per endpoint; retry does not retry client-error responses.
  • Loading UI uses isPending/isLoading correctly and distinguishes background refetches via isFetching.
  • Conditional fetching uses enabled or skipToken, never a conditional hook call.
  • Suspense queries are always prefetched on server-rendered routes, and error boundaries can be reset.
  • Mutations invalidate or update every affected key, and optimistic updates cancel in-flight queries, snapshot, and roll back (mutations.md).
  • Server rendering creates one QueryClient per request and hydrates through HydrationBoundary (ssr.md).
  • Infinite queries define initialPageParam and return undefined from getNextPageParam at the end of the list (infinite-queries.md).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.