logo
eng-flag

TanStack Query Cheatsheet

Table of Contents

  1. Installation
  2. Basic Usage
  3. Queries
  4. Mutations
  5. Query Invalidation
  6. Prefetching
  7. Infinite Queries
  8. Dependent Queries
  9. Caching and Stale Time
  10. Error Handling
  11. Optimistic Updates
  12. Pagination
  13. Server-Side Rendering (SSR)
  14. Testing
  15. Best Practices

Installation

To install TanStack Query in your project:

npm install @tanstack/react-query
# or
yarn add @tanstack/react-query

Basic Usage

First, wrap your application with QueryClientProvider:

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

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app components */}
    </QueryClientProvider>
  )
}

Queries

Queries are used to fetch data from a server. Here's a basic example:

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

function GetTodos() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('https://api.example.com/todos').then(res => res.json()),
  })

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

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

Mutations

Mutations are used to create/update/delete data:

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

function AddTodo() {
  const mutation = useMutation({
    mutationFn: newTodo => {
      return fetch('https://api.example.com/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      })
    },
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      mutation.mutate({ title: 'New Todo' })
    }}>
      <button type="submit">Add Todo</button>
    </form>
  )
}

Query Invalidation

Invalidate queries to refetch fresh data:

queryClient.invalidateQueries({ queryKey: ['todos'] })

Prefetching

Prefetch data before it's needed:

const prefetchTodos = async () => {
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('https://api.example.com/todos').then(res => res.json()),
  })
}

Infinite Queries

For pagination or infinite scrolling:

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

function InfiniteTodos() {
  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['todos'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(`https://api.example.com/todos?page=${pageParam}`).then(res => res.json()),
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'loading' ? (
    <p>Loading...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.map(todo => (
            <p key={todo.id}>{todo.title}</p>
          ))}
        </React.Fragment>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? 'Loading more...'
          : hasNextPage
          ? 'Load More'
          : 'Nothing more to load'}
      </button>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

Dependent Queries

When a query depends on the result of another:

function UserProjects() {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: projects } = useQuery({
    queryKey: ['projects', user?.id],
    queryFn: () => fetchProjects(user.id),
    enabled: !!user,
  })

  // ...
}

Caching and Stale Time

Configure caching behavior:

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 60 * 1000, // 1 minute
  cacheTime: 5 * 60 * 1000, // 5 minutes
})

Error Handling

Handle errors in queries:

const { isError, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3, // Retry 3 times before failing
  onError: (error) => {
    console.error('An error occurred:', error)
  },
})

Optimistic Updates

Update UI optimistically before server confirmation:

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
    queryClient.setQueryData(['todos', newTodo.id], newTodo)
    return { previousTodo }
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos', newTodo.id], context.previousTodo)
  },
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})

Pagination

Implement pagination:

function PaginatedTodos() {
  const [page, setPage] = useState(1)
  const { data, isLoading, isPreviousData } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
    keepPreviousData: true,
  })

  return (
    <div>
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <ul>
          {data.todos.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
      <button
        onClick={() => setPage(old => Math.max(old - 1, 1))}
        disabled={page === 1}
      >
        Previous Page
      </button>
      <button
        onClick={() => setPage(old => (!isPreviousData && data.hasMore ? old + 1 : old))}
        disabled={isPreviousData || !data?.hasMore}
      >
        Next Page
      </button>
    </div>
  )
}

Server-Side Rendering (SSR)

Configure for SSR:

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

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

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

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

function MyApp({ Component, pageProps }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

Testing

Test components using React Query:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
})

const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
)

test('TodoList shows todos', async () => {
  render(<TodoList />, { wrapper })
  expect(await screen.findByText('Todo 1')).toBeInTheDocument()
})

Best Practices

  1. Use query keys consistently
  2. Implement error boundaries
  3. Avoid over-fetching by using select
  4. Use placeholderData for better UX
  5. Implement retry logic for failed queries
  6. Use suspense mode for concurrent rendering
  7. Implement proper TypeScript types

Example of using select:

const { data: todoCount } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data) => data.length,
})

Example of using placeholderData:

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  placeholderData: previousTodos,
})

2024 © All rights reserved - buraxta.com