Lawrence Eagles Senior full-stack developer, writer, and instructor.

What’s new in React Query 3

6 min read 1875

React Query 3

Introduction

React, by default, doesn’t ship with support for things like routing, data fetching, and complex state management.

Consequently, several third-party libraries and frameworks have been developed to meet these needs.

Libraries such as React-Router and Reach Router have been developed for routing while libraries and frameworks such as Redux, Mobx, and Recoil have been developed for complex state management.

React Query is a third party library that describes itself as:

The missing data-fetching library for React; since out of the box React does not provide a way to fetch and updated data from components

React Query, however, does a lot more than fetching and updating data. It is an out of the box state management library for asynchronous data akin to Apollo Client but unlike Apollo Client, it supports both REST and GraphQL.

In a nutshell, React Query is a set of custom hooks that makes fetching, caching, and updating asynchronous or server state in React easy.

Why React Query?

One of the challenges we face when building React applications is determining an effective pattern to (fetch and update) work with server state. React does not give us anything out of the box.

Consequently, developers create their own ways by fetching data (server state) inside a useEffect hook, then copying the result into a component-based state (client state). This pattern works but it is not optimal.

Let’s demonstrate the downsides of this pattern by considering the code below:

import "./styles.css";
import React, {useEffect, useState} from "react";
import axios from 'axios';
export default function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [userData, setUserData] = useState(null);
  useEffect(() => {
    async function getUserData() {
      try {
        setIsLoading(true);
        const {data} = await axios.get(`https://jsonplaceholder.typicode.com/users/1`);
        setUserData(data);
        setIsLoading(false);
      } catch (error) {
        setIsLoading(false);
        setIsError(error);
      }
    }
    getUserData();
  }, []);
  return (
    <div>
      {isLoading && (<div> ...Loading </div>)}
      {isError && (<div>An error occured: {isError.message}</div>)}
      {userData && (<div>The username is : {userData.username}</div>)}
    </div>
  )
}

Play with code.

We made a custom demo for .
No really. Click here to check it out.

In our small contrived example above, we are fetching the user data from the https://jsonplaceholder.typicode.com/users/1 endpoint. Then we render a view base on the status (success, loading, or error) of the API call. This method has several problems such as:

  1. We have to make this call at the App component or any component high up in our component tree. This is to enable pass down data to other (nested) components that need them using prop drilling. Prop drilling in React is an anti-pattern and should be avoided by all means. Here are some strategies to help avoid prop drilling
  2. We have to repeat this boilerplate code in every component we fetch data. The above example requires both the useState and the useEffect hook and we used three different client (local) states (isLoading, isError, and userData) to determine the status of the API call. All these would have to be rewritten in every component we need to fetch data. Although we can abstract the hooks logic of this boilerplate code into a custom reusable hook, and reuse it across our app, it still does not solve all of our problems
  3. With this pattern, developers often fall into the pitfall of mixing the client state and the server state together. The resulting state object may contain some component-based (client) state e.g the sidebar status or the theme color with server state e.g the fetched user data. The final state object may look something like this:
    const [state, setState] = {
      showSideBar: false,
      theme:       "dark",
      currentUser: {},
      users:       [],
      posts:       []
    };

To avoid this trouble, some developers turn to global state management libraries like Mobx and Redux. While this may work, they add an extra layer of complexity to our application, and in some cases that can be overkill.

Also, while some traditional state management libraries are great at managing client state, they are not so efficient in handling server state.

Server state management has unique requirements for this because of the following:

  1. It’s persisted remotely and it is not in our control
  2. It’s accessible and changeable by other people
  3. It can become stale (out of date)
  4. It requires an asynchronous API for fetching and updating

Consequently, to efficiently manage server state we need:

  1. To store our server state in an in-memory cache
  2. A mechanism to know if the state has changed
  3. To periodically update the state in the background
  4. To reflect updates as quickly as possible

These are not features that we can easily code on our own. Fortunately, these are the problems React Query was created to solve.

Out of the box React Query gives us a set of hooks for fetching, caching, and updating async data (server state).

In the next section, we will elaborate on this by refactoring our boilerplate code above to use React Query.

Getting started

Installation

# NPM
npm i react-query

#Yarn
yarn add react-query

To refactor our boilerplate code above to use React Query follow the steps below:

  1. Set configurations to connect our app to React Query’s cache using the QueryClient and the QueryClientProvider like this:
    import "./styles.css";
    import React from "react";
    import { QueryClient, QueryClientProvider } from "react-query";
    import User from "./Components/User";
    // Create a client
    const queryClient = new QueryClient();
    export default function App() {
      return (
        // Provide the client to your App
        <QueryClientProvider client={queryClient}>
          <User />
        </QueryClientProvider>
      );
    }
  2. Fetch data from our component using React Query and a data fetching library like axios:
    import React from "react";
    import { useQuery } from "react-query";
    import axios from "axios";
    const User = () => {
      const fetchUser = async () => {
        const { data } = await axios.get(
          `https://jsonplaceholder.typicode.com/users/1`
        );
        return data;
      };
      const { 
               isLoading, 
               isSuccess, 
               error, 
               isError, 
               data: userData 
            } = useQuery("user",fetchUser);
      return (
        <div>
          {isLoading && <article>...Loading user </article>}
          {isError && <article>{error.message}</article>}
          {isSuccess && (
            <article>
              <p>Username: {userData.username}</p>
            </article>
          )}
        </div>
      );
    };
    export default User;

Play with code.

From the example above, we can see that by using the useQuery hook from React Query we have removed complex useEffect and useState logic from our code. This is cleaner, maintainable, and DRY.

Also, React Query stores the server state in the cache we have configured above and our components are served from there, thus enhancing performance.

React Query keeps the server state updated by periodically making API calls to the endpoint in the background. This is to ensure that our component always gets the latest server state.

There are a lot more advantages that the React Query library brings. Our small example above gives a high-level introduction to React Query and we have barely scratched the surface of its features.

In the next section, we will focus on the new awesome features added to React Query version 3.

New features

Query data Selectors

With these features React Query brings some of the good parts of GraphQL to REST. The useQuery and the useInfiniteQuery hooks now have a select option. This enables us to select or transform the desired parts of the query result.

We can now select only the desired part of the query result in our example above like this:

...
  const { 
    isLoading, 
    isSuccess, 
    error, 
    isError, 
    data: username 
  } = useQuery("user", fetchUser,{
       select: (user) => user.username
  });
...

We would then render the username like this:

...
{isSuccess && (
  <article>
    <p>Username: {username}</p>
  </article>
)}
...

See full code.

The useQueries hook

The useQueries hook is used to fetch a variable number of queries and returns an array with all the query results.

The useQueries hook takes a parameter which is an array containing different query option objects like this:

const results = useQueries([
 { queryKey: ['user', 1], queryFn: fetchUser },
 { queryKey: ['user', 2], queryFn: fetchUser },
 { queryKey: ['user', 3], queryFn: fetchUser },
 { queryKey: ['user', 4], queryFn: fetchUser },
])

Retry/offline mutations

React Query mutations never had retry but in React Query 3 you can pass a second argument to the useMutation hook to configure retry like this:

const mutation = useMutation(addUser, {retry: 3});

So if a mutation fails because the device is offline, the mutation is retried the set number of times (three times in the above case) when the device is reconnected.

However, by default, React Query will not retry a mutation if it fails.

Persist mutation

In React Query 3, a mutation can be persisted to storage using hydrate functions. This is useful if you want to pause the mutation because the device is offline and resume the mutation when the device is reconnected. You can get more on this here.

QueryObserver

The QueryObserver function works with the new operator. It is used to create or watch a query like this:

const observer = new QueryObserver(queryClient, { queryKey: 'videos' })
const unsubscribe = observer.subscribe(result => {
 // do something here.
 unsubscribe()
})

The QueryObserver can also be used to observe and switch between queries. It takes two parameters which are the queryClient and an option object. The options for the QueryObserver are identical to those of the useQuery hook.

InfiniteQueryObserver

This is similar to the QueryObserver function. It also works with the new operator but its use case is different.

The InfiniteQueryObserver hook enables us to observe and switch between infinite queries. It takes two parameters which are the queryClient and an option object as shown below:

const observer = new InfiniteQueryObserver(queryClient, {
 queryKey: 'videos',
 queryFn: fetchVideos,
 getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
 getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
}) 
const unsubscribe = observer.subscribe(result => {
  // do something
  unsubscribe()
})

The options for the InfiniteObserverQuery is exactly the same as the options for the InfiniteQuery hook.

QueriesObserver

The QueriesObserver can be used to create or observe multiple queries like this:

const observer = new QueriesObserver(queryClient, [
 { queryKey: ['users', 1], queryFn: fetchUsers },
 { queryKey: ['users', 2], queryFn: fetchUsers },
]) 
const unsubscribe = observer.subscribe(result => {
 // do something
 unsubscribe()
})

It also works with the new operator and it takes two parameters which are the queryClient and an options object.

Set default options for specific queries

The QueryClient.setQueryDefaults() method enables us to set default options for specific queries like this:

queryClient.setQueryDefaults('users', { queryFn: fetchUsers }
function GetUsers() {
 const { data } = useQuery('users')
   return data;
}

Set default options for specific mutations

This method allows us to set default options for specific mutations like this:

queryClient.setMutationDefaults('addUser', { mutationFn: addUser })
function AddUser() {
  const { mutate } = useMutation('addUser')
    // do something...
}

The useIsFetching hook

The useIsFetching hook is an optional hook that returns the number of queries your application is fetching in the background.

It now has a filter that can be used to return only the numbers of queries that are fetching, that match the filter. Consider the example below:

 ...
const isFetching = useIsFetching() // returns the how many queries are fetching
const isFetchingPosts = useIsFetching(['users']) // returns how many queries matching the users filter that are fetching.
...

Final thoughts

React Query is an awesome library that addresses the pains of managing asynchronous data when working with React. It makes working with server state a breeze.

React Query 3, as we have seen in this post, adds some awesome features to this great library. Also, the React Query core is now separated from React and it can be used standalone or with other frameworks.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Lawrence Eagles Senior full-stack developer, writer, and instructor.

Leave a Reply