TanStack Query: Streamlined Server State for Web Apps

Traditional core web frameworks often lack a standardised approach to handling asynchronous data fetching and updating in web apps. Consequently, developers resort to piecemeal solutions or rely on generic state management libraries, leading to fragmented implementations.

Further, managing server state presents inherent challenges:

  • Remote persistence: Server state resides in remote locations beyond the developer's control.
  • Asynchronous operations: Data fetching and updating requires asynchronous APIs.
  • Shared ownership: Server state can be modified by multiple parties, introducing potential conflicts.
  • Synchronisation concerns: Failure to synchronise server state can lead to outdated data in web applications.
  • Additional hurdles: Ad hoc approaches often struggle with caching, deduplication of requests, and performance optimisation.

That’s the motivation behind TanStack Query, a robust solution that addresses these challenges head-on! Designed to streamline complex tasks like data fetching, caching, synchronisations and updates in web apps, TanStack Query boosts developer efficiency and enhances user experience.

Example: Fetching Pokémon Data

Let's dive into a sample React application written in JavaScript, leveraging TanStack Query to fetch data from the Poké API. You can explore the source files on CodeSandbox for a hands-on experience.

Our goal is to achieve the following tasks seamlessly:

  1. Retrieve Pokémon Data: We’ll send a GET request to https://pokeapi.co.api/v2/pokemon?limit=12 to fetch general data for 12 Pokémon.
  2. Fetch Additional Details: Next, we’ll send a GET request to each Pokémon’s url property from the previous response to retrieve its image and type.
  3. Display Pokémon Information: Finally, we’ll showcase the name, image, and type of each Pokémon in visually appealing cards using React and CSS.

Implementation

We’ll start by defining two functions to handle asynchronous HTTP requests to the API endpoints. While we’ll use Axios in this example out of preference, the Fetch API equally works great and is used in the official example.

const fetchPokemons = async () => {
  const { data } = await axios.get('https://pokeapi.co.api/v2/pokemon?limit=12')
  const pokemons = await Promise.all(data.results.map(getPokemonData))
  return pokemons
}

const getPokemonData = async ({ url }) => {
  const {
    data: { id, name, sprites, types }
  } = await axios.get(url)

  return {
    id,
    name,
    img: sprites.other['official-artwork']?.front_default,
    pokemonType: types[0].type.name
  }
}

In the fetchPokemons function, we fetch general data for 12 Pokémon from the Poké API. We then use Promise.all to fetch additional details for each Pokémon in parallel.

The getPokemonData function retrieves detailed information for a single Pokémon, including its ID, name, image, and type. We destructure nested data from the JSON response for ease of access.

Next, we integrate TanStack Query in our sample app (App.js) by wrapping our component tree in a QueryClientProvider. Also, we initialize a QueryClient and pass it to the provider:

import { QueryClient, QueryClientProvider } from 'react-query'

import Pokemons from './components/Pokemons'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Pokemons />
    </QueryClientProvider>
  )
}

By providing the QueryClient to the QueryClientProvider, TanStack Query gains access to the query client object, enabling us to utilize hooks from the library in descendant components. This setup allows us to effortlessly make queries to fetch data from the server.

We can now employ the useQuery hook, which allows us to initiate queries with a unique key and a function that returns a promise:

const result = useQuery({ queryKey, queryFn })

The result object returned by useQuery provides essential information about the query, including:

  • status: Indicates whether the query is pending, successful, or encountered an error.
  • error: If the query fails, the error message is accessible via the error property.
  • data: Upon successful completion, the fetched data is available in the data property.

The queryKey is used to identify data in the cache; it enables rapid retrieval of existing data and background fetching of the latest data from the server. This key can take various forms such as a string, an array, or a serialised object.

In our sample app (Pokemons.js), we implement the useQuery hook as follows:

import { useQuery } from 'react-query'

import fetchPokemons from '../api'

export default function Pokemons() {
  const {
    data: pokemons,
    isLoading,
    error
  } = useQuery({ queryKey: ['pokemons'], queryFn: fetchPokemons })

  if (error) return <p>{error.message}</p>
  if (isLoading) return <p>Loading....</p>

  return (
    <div>
      {pokemons.map(({ id, img, name, pokemonType }) => (
        <div key={id}>
          <img src={img} alt={name} />
          <div>
            <h2>{name}</h2>
            <span>{pokemonType}</span>
          </div>
        </div>
      ))}
    </div>
  )
}

Here, we access the isLoading state to indicate that the query is in progress. In case of errors, we handle them gracefully, displaying an error message. Once the data is successfully fetched, we iterate over the Pokémon array and render relevant information.

With TanStack Query, the fetched data is readily available in the cache, accessible for direct use or manipulation in other components. Additionally, the useQuery hook offers a configurable object for fine-tuning query behavior, allowing control over caching, retry policies, error handling, and more.

Furthermore, if server-side state updates are necessary, TanStack Query provides the useMutation hook for seamless integration.

Here’s a screenshot of our sample app:

Screenshot from the sample pokémon app with 12 cards each showing the name, image and ability of the pokémon

Summary

TanStack Query empowers developers to write cleaner and more efficient code while delivering robust and performant web applications. Its intuitive design and powerful features make it a valuable asset for optimising server state management.

To explore further and harness the full potential of this powerful library, visit the docs.