Introduction to React Query
8 min read

Introduction to React Query

Introduction to React Query

Data is at the heart of modern web apps, and React is the reigning UI framework.


Application state is everywhere—from determining whether a button is enabled, user input in forms, and remote state stored in your database behind an API. It's buried in React components, encapsulated in stores like Redux, URLs, and HTML elements.


React Query offers a stable, mature, and production-ready set of hooks to interact with the data your React app produces and consumes.


Why do you need React Query?


React Query solves the core problem facing modern single page web apps - how to manage state, especially remote state fetched over unreliable networks and stored in memory in your single page application.


Declarative State Management


State management in React can get complicated fast. You need a predicable way to access and mutate your state.


You can choose from one of the myriad state management libraries available today, but this only solves part of the problem. State management libraries by and large help you manage runtime state in your application.


However, this ignores the fact that most applications are backed by remote state stored in a DB behind an API or in a 3rd party service.


Your application state often needs to be hydrated by fetching and mutating this remote state before storing it in either component state or a global store like Redux. It then needs synced and mutated along with changes to the remote state.


React Query's hook API provides a declarative way to fetch and mutate remote state. You declare a query or a mutation using a hook from React Query, and React Query takes care of query retries, caching responses, loading/fetching/error states, refetching.


Caching


Data gets stale. People leave browser tabs open for days or weeks or open your app simultaneously on 4 tabs.


Applications need a strategy for clearing out any data that is no longer valid.
Additionally, apps need a way to hold on to data long enough to avoid refetching the same bit over and over again.


Under the hood, React Query maintains a cache of your queries along with a unique key. You can configure React Query to refetch and update the cache at set intervals, when the network state changes (e.g., a reconnect), when the browser tab regains focus.

Unpredictable Networks


Networks are inherently unreliable. A robust app needs to handle dropped requests, lost network connections, and other network failures.
Handling all of these possibilities every time you make a network request can lead to whack-a-mole troubleshooting.
React Query comes ready with automatic query retries and connectivity change detection so that you can gracefully handle degraded network conditions.
By default, a query is tried 3 times on failure before reporting a failure - but this is fully configurable. You can define an exponential back-off, number of retries, disable query retries, and just about any other strategy your application needs. (Note, however, that mutations are not retried automatically.)

Optimistic UIs


Users expect the UI to respond within 100ms, otherwise the UI can feel laggy and unresponsive. Nobody likes to sit and watch loading state after clicking a button.
But, sometimes the network request will take _at least 100ms_, accounting for network speed, latency, server response times, and other unpredictable factors like the user's hardware.


Optimistic UIs helps your app feel snappy, updating the UI while the network request happens in the background. But, rolling your own optimistic UI is complicated and involves the following steps:

  1. Update the application state to reflect success scenario
  2. Render components according to successful state
  3. Trigger a network request
  4. Error handling to rollback/gracefully handle a failed mutation
  5. Additional state mutations on success


React Query provides cache mutation methods so you can declaratively do optimistic state transforms and rollbacks in the case of failure. If you treat the query cache as the source of truth, you can perform operations against the query cache in the mutation and your components will react and render the appropriate state.


Core Concepts with Examples


React Query does not care about how the data is fetched but the code examples use axios.

Setup

React Query is open source and available via npm:

 $ npm i react-query
 # or
 $ yarn add react-query

You only need a to wrap your application in a single context that provides the QueryClient for consumption:

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

const queryClient = new QueryClient()

 function App() {
   return (
     <QueryClientProvider client={queryClient}>
       <YourApp />
     </QueryClientProvider>
   )
 }

The QueryClient can be instantiated with no arguments as in this example, or you can set a number of options that control the global behavior of querying and mutations.

In most cases, React Query has chosen sane defaults are fine for getting started but you definitely want to review the options before going to production.


Queries

The `useQuery` hook is responsible for fetching and storing remote state in the query cache:

import { useQuery } from 'react-hooks'
import * as axios from 'axios'
import Loading from './Loading'

const getPosts = () => axios.get('/posts').then(res => res.data)

const Posts = () => {  
  const { data, isLoading } = useQuery(
    ['posts'], // the "query key"
    getPosts
  )
  if (isLoading) {
    return <Loading />
  }
  return <ul>
    {data.map(post => <li>
      <Post post={post} />
    </li>)}
  </ul>}
 }


This is a query in its most basic form. React Query does not care _how_ you perform the query under the hood - just that the query function resolves with the data.
The query key - in this case `['posts']` - is extremely important: it's how React Query uniquely identifies this query and stores it in the cache.
It's common to use array-style query keys because they're easy to perform inclusive matching on. For example, you might have query keys like so:


* `['posts']` - list of posts

* `['posts', 1]` - post with ID of 1

* `['posts', 1, 'comments']` - post 1's comments


Using the array format with a common prefix—in this example `'posts'`—allows you take take advantage of query filters that target everything related to the hierarchy of data.

For instance, perhaps you want to refetch everything related to posts, you would simply do:

await queryClient.refetchQueries('posts', {
  exact: false, // match inclusively for keys that start with 'posts'    
  active: true  // only queries bound to an active React component
})


Mutations


The `useMutation` hook provides a way to mutate remote state. Let's define a way to delete a post:

import { useMutation } from 'react-hooks'
import * as axios from 'axios'

const Post = ({ post }) => {
  const {
    mutate,
    isLoading,
    isSuccess
  } = useMutation(
    () => axios.delete(`/posts/${post.id}`)
  );
  
  if (isSuccess) {
    // this post has been deleted
    return null
  }
  return <>
    <h3>{post.name}<h3>
    <button onClick={() => mutate()}
    disabled={isLoading}>delete post</button>
  </>
 }


Mutations do not automatically update the query cache, but React Query provides a number of ways to perform those updates that we'll go through below.


Query Client


The query client is the heart of React Query's data management. It's keeping track of queries, metadata about the queries (like whether or not they've gone stale), and responses.


The query client is available throughout your components or hooks via a hook:

import { useQueryClient } from 'react-query'

const queryClient = useQueryClient()


The `queryClient` lets you grab hold of queries or data in your cache:

// get posts by the query key
const posts = queryClient.getQueryData(['posts']);

// invalid
await queryClient.invalidateQueries(  ['posts'],  {
    // match the query key inclusively -
    // so ['posts'] and ['posts', 1] 
    exact: false,
    
    // if the query is still related to a mounted component,
    // prompt an immediate refecth
    refetchActive: true,
    
    // if the query is not related to a mounted component,
    // do not refecth
    refetchInactive: false
})


In many basic cases, you don't need to interact with the query client directly; advanced uses like optimistic UIs need to interact with the query client much more.


In the case of our delete posts, let's look at one way we could handle updating the query cache:

const queryClient = useQueryClient();

const {
  mutate,
  isLoading,
  isSuccess
} = useMutation(
  () => axios.delete(`/posts/${post.id}`),
  onSuccess() {
    queryClient.invalidateQueries(['posts'], {
      exact: true
    });
  }
)


Once the `['posts']` query is invalidated, React Query will trigger a refetch of that one query automatically and rerender the posts state - now without the deleted post.



Custom Hooks


The examples above uses React Query's hooks like `useQuery` and `useMutation` directly inside of the components - but you're not limited to using the raw hooks from React Query but can compose the primitives exposed by the library into your own hooks.


This is fine, but it does have the limitation of not being very reusable. Every time you want to perform a query or mutation you need to rewrite the entire query/mutation logic. However, you often need to access this from multiple components in your application.


Custom hooks are a great way to centralize your data fetching logic.

// posts.hooks.js
import axios from 'axios'
import { useQuery, useMutation, useQueryClient } from 'react-query'

export const usePosts = () => useQuery(
  ['posts'],
  () => axios.get('/posts').then(res => res.data)
)

export const usePost = (id) => useQuery(
  ['posts', id],
  () => axios.get(`/posts/${id}`).then(res => res.data)
)

export const useDeletePost = (id) => {
  const queryClient = useQueryClient()
  return useMutation(
    (id) => axios.delete(`/posts/${id}`),
    {
      onSuccess() {
        // invalidate just the query for fetching posts
        queryClient.invalidateQueries(['posts'], {
          exact: true
        })
      }
    }
  )
}


This approach has a number of benefits:

  • centralizes common query and mutation logic
  • all your query keys are defined in one spot
  • hooks have semantic names that are easy to identify


Real-World Example


Chime Social uses React Query to manage all of its data fetching and mutation. Chime Social is a Twitter scheduling and analytics tool, so the example shows how Tweets are scheduled.

Chime Social's calendar view


Fetching Scheduled Tweets


When the calendar is rendered, we need to grab hold of a week's worth of scheduled Tweets to render the user's schedule.


Chime supports a number of things that React Query makes easy:

  • Managing multiple Twitter user's schedule at once
  • Keeping the schedule view up to date with the current time
  • Fetching a week of Tweet's at a time
import { useQuery } from 'react-query'
import { useActiveUser } from './utils'
import { httpClient } from './client'

export const useSchedule = ({
  start,
  end
}) => {

  // Chime supports scheduling for multiple Twitter accounts.
  // This hook gets the active user and sets it as part of the
  // query key, which means whenever the user changes - the 
  // schedule is automatically reloaded with the current user
  const user = useActiveUser();
  return useQuery(
    [
      'schedule',
      user.id, 
      start ? start.getTime() : null,
      end ? end.getTime() : null,
    ].filter(Boolean),
    () =>
      httpClient.get(
        `/api/v1/user/${user.id}/schedule/tweets.json?${stringify({
          start: start ? start.getTime() : new Date().getTime(),
          end: end ? end.getTime() : undefined,
        })}`,
        {
          headers: defaultHeaders,
        },
      )
      .then(({ data }) => data),
    {

      // retry failed queries with exponential backoff
      retryDelay: exponentialBackoffRetry,

      // by default, the cache is declared stale every 5 mins
      // refetch every 3 mins to stay ahead of cache clearing
      // and to 
      refetchInterval: 3 * 60 * 1000,

      // refetch even when the tab isn't active
      refetchIntervalInBackground: true,

      // tabs tend to stay open for awhile - always
      // show an up-to-date schedule when they refocus
      // on the tab
      refetchOnWindowFocus: 'always' ,
    },
  );
}

Then, when the calendar component fetches the user's schedule, it's quite simple:

import { useState } from 'react'
import { startOfWeek } from 'date-fns'
import { useSchedule } from './queries'

const Calendar = () => {
  const [startDate, setStartDate] = useState<Date>(startOfWeek(new Date()));
  const { data: schedule, isError, isLoading } = useSchedule({
    start: startDate,
    end: addDays(startDate, 7)
  });

  if (isError) {
    // err
    return <ErrorView />
  }
  if (isLoading) {
    return <LoadingView />
  }

  return <CalendarGrid schedule={schedule} />
}

Creating Tweets

Of course, it would be a Tweet scheduling tool if you couldn't actually schedule a post.

In this case, we use a mutation along with a success handler to update the query cache:

import { useMutation } from 'react-query'
import { useActiveUser } from './utils'
import { httpClient } from './client'

export const useCreateThread = () => {
  const user = useActiveUser()

  return useMutation(
    (thread) => {
      return client
        .post(`/api/v1/user/${user.id}/schedule/threads.json`, rest, {
          headers: defaultHeaders,
        })
        .then(({ data }) => data)
    },
    {

      // After the thread is created, we push the thread
      // into either the drafts or regular schedule
      onSuccess(newThread) {
        const isDraft = !newThread.scheduled_at;
        if (isDraft) {
          queryClient.setQueryData(
            ['drafts', user.id],
            (tweets) => {
              const updated = [...(tweets || [])];
              updated.push(newThread);
              updated.sort((a, b) => b.id - a.id);
              return updated;
            },
          );
        } else {
          queryClient.setQueryData(
            ['schedule'],
            ({ tweets, before, after }) => {
              const updated = [...(tweets || [])];
              updated.push(newThread);
              updated.sort((a, b) => b.scheduled_at - a.scheduled_at);
              return {
                tweets: updated,
                before,
                after,
              };
            },
          );
        }
      },
    },
  );
};


Wrapping Up

If you're managing remote state in a single page React application, React Query can help you write product-grade state handling in few lines of code.

It's easy to get started with in a new project—or progressively adopt in an existing project but just wiring in queries and mutations in a single part and slowly migrating your existing data fetching solution.

Enjoying these posts? Subscribe for more