TanStack Query Introduction

TanStack Query (also known as React Query) is a powerful library for managing server state in web applications. Concretely speaking, TanStack Query enables the simplification of operations such as data fetching, caching, synchronizing, and updating server state in web applications.

Some of the key concepts in TanStack Query are:

  • Data Fetching – TanStack Query provides an efficient way of fetching data from APIs or other data sources.
  • Query Cache – TanStack Query can automatically store data gathered from API responses, making forthcoming request for the same data faster by getting it from the cache.
  • Mutations – TanStack Query provides a custom hook, that simplifies data modification on the server side (POST, PUT, and DELETE requests).
  • Automatic Refetching – TanStack Query provides options for automatic data refetching based on given conditions.
  • Pagination and Infinite Loading – TanStack Query has built-in support for pagination and infinite loading patterns.

 

Queries vs Mutations

Queries 

A query represents a request for specific data from a server or some other data source. Each query must be identified by a unique query key. Query keys are important because TanStack Query manages caching, refetching and sharing queries based on them. The unique key is represented by an array. Arrays can be as simple as an array of one string, but they can also be much more complex arrays that contain multiple strings and nested objects as elements. 

TanStack Query provides a hook called useQuery for working with queries. This hook expects an object parameter with at least two properties:

  • queryKey – usage of query keys is explained above
  • queryFn – this is a function that returns a promise that either resolves the data or throws an error
import { useQuery } from '@tanstack/react-query'
function App() {
  const result = useQuery({ queryKey: ['books'], queryFn: fetchBooks })
}

The result object contains a few very useful fields. Some of the most useful ones are:

  • data – field that contains the fetched data
  • error – field that gives info about the error if the fetch fails
  • isFetching – a field that is set to true while data is being fetched and after the request is completed either successfully or unsuccessfully is set back to false
  • isLoading – this field is set to true if the query has no data present
  • isError – this field is set to true if the query encounters an error
  • isSuccess – this field is set to true if the query was successful and the fetched data is present
  • refetch – this field is a function that can be used to manually refetch the data

Mutations

Unlike queries, mutations are typically used to modify the data or perform server side-effects. TanStack Query provides a useMutation hook for performing mutations. This hook expects an object parameter with at least one property:

  • mutationFn – represents a function that performs the actual mutation operation typically by sending a request to server API for creating, updating, or deleting data.
function App() {
  const mutation = useMutation({
    mutationFn: (newBook) => {
      return axios.post('/books', newBook)
    },
  })
 ...
}

Similarly to useQuery, useMutation also returns an object with some useful properties. Data and error properties can be used similarly to the useQuery. To trigger the mutation function, the mutate property is returned from useMutation. It is important to point out that the result of the mutation isn’t stored in a cache, so the mutation will be called every time, even if the same parameters are in use.

useMutation comes with helper options that can be used to perform side effects during any stage of the mutation life-cycle:

  • onMutate – called before the mutation operation is executed
  • onSuccess – called if the mutation operation is successfully executed
  • onError – called if the mutation operation failed to execute
  • onSettled – called after the mutation operation is finished, no matter if it succeeded or failed 

Functions that are called should be defined by the developer and set as values for these properties; useMutation only provides these properties, so it is known in which case any of these functions will be called.

Parallel vs Dependent Queries

Parallel queries

Parallel queries represent requests that can be executed in parallel (at the same time) to maximise performance and responsiveness. If the number of queries needed to be executed is constant, parallel queries can be executed simply by calling the useQuery hook multiple times.

function App () {
  // The following queries will execute in parallel
  const booksQuery = useQuery({ queryKey: ['books'], queryFn: fetchBooks })
  const movies = useQuery({ queryKey: ['movies'], queryFn: fetchMovies })
  const games = useQuery({ queryKey: ['games'], queryFn: fetchGames })
  ...
}

If the number of queries executed varies from render to render, the useQueries hook should be used. This hook accepts a configuration object as a parameter that has a queries key that accepts an array of query objects. It then returns an array of query results.

function App({ users }) {
  const bookQueries = useQueries({
    queries: books.map((user) => {
      return {
        queryKey: ['book', book.id],
        queryFn: () => fetchBookById(book.id),
      }
    }),
  })
}


Dependent queries

Dependent queries depend on other queries to be finished before they can be executed. This can be implemented using the enabled key to tell the query when it can be executed. 

// Get the student
const { data: student } = useQuery({
  queryKey: ['student', email],
  queryFn: getStudentByEmail,
})
const studentId = student?.id
// Then get the student’s grades
const {
  data: grades,
} = useQuery({
  queryKey: ['grades', studentId],
  queryFn: getGradesForStudent,
  // The query will not execute until the studentId exists
  enabled: !!studentId,
})

If enabled is set to false, fields isLoading and isFetching are also set to false since the query won’t be loading data automatically. However, these values can still be true if developer manually triggers the fetch by using refetch function.

Dependent queries can also be implemented with useQueries hook, which can not be achieved with enabled property.

// Get the student ids
const { data: studentIds } = useQuery({
  queryKey: ['students'],
  queryFn: getStudentIds,
  select: (students) => students.map((student) => student.id),
})
// Then get the students emails
const studentEmails = useQueries({
  queries: studentIds
    ? studentIds.map((id) => {
        return {
          queryKey: ['email', id],
          queryFn: () => getEmailsFromStudents(id),
        }
      })
    : [], // if studentIds is undefined, an empty array will be returned
})

Dependent queries hurt the performance because of request waterfall. If it is necessary to execute two requests, sequential execution would take twice as much time as parallel execution, so it is always better to restructure the backend APIs so that requests can be executed in parallel as much as possible, but that can’t always be feasible.

Caching

But how does the caching work? 

A unique queryKey is provided for every query that identifies the data being fetched.  When a query is executed, TanStack Query first checks if the data for the corresponding queryKey is present in the cache. If the data is already cached locally and is considered fresh according to some parameters (staleTime and gcTime) this data is immediately returned in that way, avoiding unnecessary network requests. If data is considered stale, it is necessary to fetch it from the server.  While the data is being fetched, TanStack Query returns a loading state to the component to indicate that data is being fetched.

Important Defaults

staleTime – This parameter specifies the duration in milliseconds for which data is considered fresh. During this period, data won’t be automatically refetched. This helps improve the performance and reduces the number of unnecessary requests. By default, this parameter is set to 0, so data is considered stale immediately after being fetched. This setting secures that data is always up to date but may lead to unnecessary API calls.

gcTime – This parameter specifies the amount of time in milliseconds that cached data should be stored in memory after all instances of useQuery using it have been unmounted. By default, it is set to 5 minutes (3000 milliseconds), which means that if the data is not used for this duration, TanStack Query will remove it from the cache.


“Asynchronous State Management with TanStack Query” Tech Bite was brought to you by Filip Džebo, Software Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Leave a comment

Your email address will not be published. Required fields are marked *