| | |

TanStack Query 17 ⚛️ Cheatsheet

⏱️ Estimated read: 3 min | |

Installation

npm install @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
PackageCommandPurpose
Corenpm install @tanstack/react-queryRequired
DevToolsnpm install -D @tanstack/react-query-devtoolsDevelopment only
ESLint pluginnpm install -D @tanstack/eslint-plugin-queryOptional quality
RequirementValue
React version18.0+ (v5)
v4 for React 17npm install @tanstack/react-query@4
v5 hook syntaxObject only: useQuery({ queryKey, queryFn })
v4 → v5 renamecacheTime → gcTime
v4 → v5 flagisLoading → isPending for initial state

Setup

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,
      gcTime: 1000 * 60 * 60,
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

useQuery

const { data, isPending, isError, error, isFetching, refetch } = useQuery({
  queryKey: ['todos', { status }],
  queryFn: async () => {
    const res = await fetch(`/api/todos?status=${status}`)
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  },
  staleTime: 30_000,
  enabled: !!status,
  placeholderData: keepPreviousData,
  select: (todos) => todos.filter((t) => !t.done),
})
OptionPurpose
queryKeyCache address — include every queryFn dependency
queryFnMust return a promise and throw on HTTP errors
staleTimeHow long data is fresh (default 0)
gcTimeHow long inactive data is kept (default 5 min)
enabledDefer the fetch until condition is true
placeholderDataKeep previous data while fetching
selectTransform data before returning
refetchIntervalPoll on an interval
FlagMeaning
isPendingNo data yet
isSuccessData available
isErrorQuery function threw
isFetchingAny fetch running (incl. background)
isPlaceholderDataData is from previous key

useMutation

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: async (newTodo) => {
    const res = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newTodo),
    })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  },
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  onError: (error) => console.error(error.message),
})

mutation.mutate({ title: 'New todo' })
Option / ResultPurpose
mutationFnPromise-returning write function
onMutateBefore request — for optimistic updates
onSuccessAfter success — for invalidation
onErrorAfter failure — for rollback
onSettledAfter success or failure
mutate(vars)Fire-and-forget trigger
mutateAsync(vars)Promise-returning trigger
isPendingRequest running
reset()Clear mutation state

Cache Management

queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true })
queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'none' })
queryClient.setQueryData(['todos', id], updated)
queryClient.removeQueries({ queryKey: ['todos', id] })
MethodPurpose
invalidateQueriesMark stale and refetch active
setQueryDataWrite directly to cache
getQueryDataRead from cache
removeQueriesDelete cache entry
resetQueriesReset to initial state
cancelQueriesCancel in-flight request
refetchTypeBehavior
'active'Refetch active only (default)
'inactive'Refetch inactive only
'all'Refetch both
'none'Mark stale only

useInfiniteQuery

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

const allPosts = data?.pages.flatMap((page) => page.posts) ?? []
ItemMeaning
data.pagesArray of fetched pages
data.pageParamsArray of parameters
fetchNextPageLoad next page
hasNextPageWhether next page exists
isFetchingNextPageNext-page fetch running
initialPageParamFirst param (v5 required)
getNextPageParamNext param or undefined
maxPagesLimit pages in memory

useQueries (Parallel)

const results = useQueries({
  queries: ids.map((id) => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
  combine: (results) => ({
    total: results.reduce((sum, r) => sum + (r.data?.count ?? 0), 0),
    isLoading: results.some((r) => r.isPending),
  }),
})

Testing

export function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: { queries: { retry: false, gcTime: Infinity } },
    logger: { log: console.log, warn: console.warn, error: () => {} },
  })
}

export function createWrapper() {
  const queryClient = createTestQueryClient()
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )
}

const { result } = renderHook(() => useTodos(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))

Defaults Reference

OptionDefault
staleTime0
gcTime5 minutes
retry3
refetchOnWindowFocustrue
refetchOnReconnecttrue

Common Pitfalls

PitfallFix
Query refetches constantlyRaise staleTime above 0
Stale data displayedAdd the dependency to queryKey
Cache bleeds between testsFresh QueryClient per test
Error test times outretry: false in test client
Mutation does not refresh queryAdd invalidateQueries in onSuccess
Infinite query has one pagegetNextPageParam must return a cursor
isLoading used for background refetchUse isFetching instead

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!