React logoReactv5INTERMEDIATE

TanStack Query

TanStack Query cheat sheet with useQuery, useMutation, caching strategies, optimistic updates, and server state management examples.

10 min read
tanstack-queryreact-queryreactdata-fetchingcacheserver-statemutations

Sign in to mark items as known and track your progress.

Sign in

Setup & Configuration

Installing and configuring TanStack Query

Installation & Basic Setup

Setting up TanStack Query in your React application

typescript
# Installation
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools

// App.tsx - Setup QueryClient & Provider
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app components */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
💡 DevTools are essential for debugging cache and queries
⚡ Configure default options to avoid repetition
📌 gcTime (garbage collection) was formerly called cacheTime
🟢 Start with default settings, optimize as needed
setupconfiguration

Queries

Fetching and caching server data

Basic Query

Using useQuery for data fetching

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

// Basic query
function Profile() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['profile'],
    queryFn: fetchProfile
  })

  if (isLoading) return 'Loading...'
  if (error) return 'An error occurred: ' + error.message

  return <div>{data.name}</div>
}

// Query with parameters
function User({ userId }) {
  const { data, error, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId // Only run if userId exists
  })

  return <div>{data?.name}</div>
}
💡 Query keys must be unique and serializable arrays
⚡ Use enabled option to conditionally fetch data
📌 isLoading vs isFetching: loading is initial, fetching includes refetch
🟢 Always handle loading and error states
queriesfetching

Dependent Queries

Queries that depend on other queries

typescript
// Sequential dependent queries
function UserProjects({ userId }) {
  // First query
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId)
  })

  // Second query depends on first
  const projectsQuery = useQuery({
    queryKey: ['projects', userQuery.data?.id],
    queryFn: () => fetchProjects(userQuery.data.id),
    enabled: !!userQuery.data?.id // Only run when user data is available
  })

  return (
    <div>
      <h1>{userQuery.data?.name}</h1>
      <ul>
        {projectsQuery.data?.map(project => (
          <li key={project.id}>{project.name}</li>
        ))}
      </ul>
    </div>
  )
}
💡 Use enabled option to control query execution order
⚡ Each query maintains its own cache independently
📌 Handle loading states for each dependent query
🟢 Structure query keys hierarchically for clarity
queriesdependentsequential

Parallel Queries

Running multiple queries simultaneously

typescript
import { useQueries } from '@tanstack/react-query'

// Multiple parallel queries with useQueries
function Dashboard({ userIds }) {
  const userQueries = useQueries({
    queries: userIds.map(id => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
      staleTime: 1000 * 60 * 5
    }))
  })

  const isLoading = userQueries.some(query => query.isLoading)
  const isError = userQueries.some(query => query.isError)

  if (isLoading) return 'Loading users...'
  if (isError) return 'Error loading users'

  return (
    <div>
      {userQueries.map((query, index) => (
        <div key={userIds[index]}>
          {query.data?.name}
        </div>
      ))}
    </div>
  )
}
💡 useQueries for dynamic arrays, individual queries for fixed count
⚡ Parallel queries execute simultaneously for better performance
📌 combine option in v5 helps aggregate results
🟢 Promise.all in single query for tightly coupled data
queriesparallelmultiple

Mutations

Creating, updating, and deleting data

Basic Mutations

Using useMutation for data modifications

typescript
import { useMutation, useQueryClient } from '@tanstack/react-query'

function CreatePost() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const formData = new FormData(e.target)
        mutation.mutate({
          title: formData.get('title'),
          content: formData.get('content')
        })
      }}
    >
      <input name="title" />
      <textarea name="content" />
      <button type="submit">
        {mutation.isPending ? 'Creating...' : 'Create Post'}
      </button>
      {mutation.isError && (
        <div>Error: {mutation.error.message}</div>
      )}
    </form>
  )
}
💡 onMutate enables optimistic updates for instant UI feedback
⚡ mutateAsync returns a promise for async/await usage
📌 Always invalidate related queries after mutations
🟢 Use mutate for fire-and-forget, mutateAsync when you need the result
mutationscreateoptimistic

Update & Delete Mutations

Implementing update and delete operations

typescript
// Update mutation
function EditPost({ post }) {
  const queryClient = useQueryClient()

  const updateMutation = useMutation({
    mutationFn: ({ id, ...data }) => updatePost(id, data),
    onSuccess: (data) => {
      // Update specific item in cache
      queryClient.setQueryData(['post', data.id], data)
      // Invalidate list
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    }
  })

  const deleteMutation = useMutation({
    mutationFn: deletePost,
    onSuccess: (_, postId) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: ['post', postId] })
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    }
  })

  return (
    <div>
      <button onClick={() => updateMutation.mutate({
        id: post.id,
        title: 'Updated Title'
      })}>
        Update
      </button>
      <button onClick={() => deleteMutation.mutate(post.id)}>
        Delete
      </button>
    </div>
  )
}
💡 Update specific cache entries with setQueryData
⚡ Optimistic deletes provide instant feedback
📌 Always provide rollback logic in onError
🟢 Use removeQueries to clean up deleted item caches
mutationsupdatedeletecrud

Infinite Queries

Implementing pagination and infinite scroll

Infinite Scroll

Loading data progressively with useInfiniteQuery

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

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

  if (isLoading) return 'Loading...'
  if (isError) return 'Error!'

  return (
    <div>
      {data.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? 'Loading more...'
          : hasNextPage
          ? 'Load More'
          : 'Nothing more to load'}
      </button>
    </div>
  )
}
💡 Use intersection observer for automatic loading
⚡ getNextPageParam determines pagination logic
📌 pages array contains all loaded pages in order
🟢 Provide manual load button as fallback
infinitepaginationscroll

Cache Management

Managing and manipulating the query cache

Cache Operations

Working with the QueryClient cache

typescript
import { useQueryClient } from '@tanstack/react-query'

function CacheControls() {
  const queryClient = useQueryClient()

  // Invalidate queries
  const handleInvalidate = () => {
    // Invalidate all queries
    queryClient.invalidateQueries()

    // Invalidate specific queries
    queryClient.invalidateQueries({ queryKey: ['posts'] })

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

  // Prefetch data
  const handlePrefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['post', 2],
      queryFn: () => fetchPost(2),
      staleTime: 1000 * 60 * 5
    })
  }

  // Direct cache manipulation
  const handleCacheUpdate = () => {
    // Set data directly
    queryClient.setQueryData(['user'], { name: 'John' })

    // Get cached data
    const cachedUser = queryClient.getQueryData(['user'])

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

  return (
    <div>
      <button onClick={handleInvalidate}>Invalidate</button>
      <button onClick={handlePrefetch}>Prefetch</button>
      <button onClick={handleCacheUpdate}>Update Cache</button>
    </div>
  )
}
💡 Invalidation marks data stale and triggers refetch
⚡ Prefetch data before users need it for instant loading
📌 setQueryData for optimistic updates without server call
🟢 Use exact: true for precise cache key matching
cacheinvalidationprefetch

Advanced Patterns

Advanced TanStack Query patterns and techniques

Suspense & Error Boundaries

Using React Suspense with TanStack Query

typescript
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'

// Using suspense query
function Profile({ userId }) {
  // This will suspend
  const { data } = useSuspenseQuery({
    queryKey: ['profile', userId],
    queryFn: () => fetchProfile(userId)
  })

  // Data is guaranteed to be defined
  return <div>{data.name}</div>
}

// App with Suspense and Error Boundary
function App() {
  return (
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <Suspense fallback={<div>Loading...</div>}>
        <Profile userId={1} />
      </Suspense>
    </ErrorBoundary>
  )
}
💡 useSuspenseQuery guarantees data is always defined
⚡ Combine with ErrorBoundary for complete error handling
📌 QueryErrorResetBoundary resets queries on error recovery
🟢 Great for SSR and concurrent features
suspenseerror-boundaryadvanced

Custom Hooks & Patterns

Building reusable query hooks and patterns

typescript
// Custom hook for user data
function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId,
    staleTime: 1000 * 60 * 5,
  })
}

// Custom hook with mutations
function useUpdateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: updateUser,
    onSuccess: (data) => {
      queryClient.setQueryData(['user', data.id], data)
      queryClient.invalidateQueries({ queryKey: ['users'] })
    }
  })
}

// Usage
function Profile({ userId }) {
  const { data: user, isLoading } = useUser(userId)
  const updateUser = useUpdateUser()

  if (isLoading) return 'Loading...'

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}>
        Update
      </button>
    </div>
  )
}
💡 Create custom hooks to encapsulate query logic
⚡ Generic CRUD hooks reduce boilerplate significantly
📌 Debounced queries prevent excessive API calls
🟢 Keep hooks focused on single responsibility
hookspatternsreusable