React Query 3: A Guide to Fetching and Managing Data

Share this article

React Query 3: A Guide to Fetching and Managing Data

Building front-end CRUD applications often starts out easy then turns complicated as you continue adding features. For every API endpoint, you’ll need to deal with state management, synchronization, caching and error handling. In this article, you’ll learn about a library called React Query and how it can help resolve all those issues. The library describes itself as the “missing data-fetching library” providing “server state management” for your React application.

We’ll be using a complete React Query Demo project to learn about the main features the library provides. You’ll then be able to apply this knowledge into your own projects. First, let’s get acquainted with a number of items before commencing with project setup.

React Query 3

About React Query 3

React Query is an open-source project created by Tanner Linsey. The latest major version, React Query 3, was officially released in December 2020. With this new version, new features were added and existing ones were improved.

You should be aware that there’s a number of breaking changes from React Query 2.x, which was quite popular before the new version came out. There’s a migration guide which clearly explains these changes, as you’ll probably come across a lot of outdated tutorials that were written for the older version.

The new version offers vast improvements and many of the bugs reported earlier have been resolved. Version 3, though ready for production, is still undergoing development as new bugs get squashed on a regular basis.

Prerequisites

This article is written for intermediate to advanced front-end developers who’ve grasped fundamental skills and knowledge in:

In your developer’s machine environment, you’ll need to have set up the following:

With that out of the way, let’s get into setting up the demo project.

About the Project

The demo project that we will analyze is a React front-end application that will use React Query to fetch data provided by a REST JSON API server. The app is only made up of five pages that showcase the features that React Query provides. These features include:

  • Basic Query
  • Paginated Query
  • Infinite Query
  • Create Mutation
  • Update Mutation
  • Delete Mutation

React Query provides many more features that unfortunately are outside the scope of this article. Below is a preview of the application we’ll be working with.

react query preview

Project Setup

Before we start setting up, I think it’s best to briefly familiarize yourself with additional dependencies used in the project. These include:

  • Vite: a very fast build tool
  • WindiCSS: a very fast Tailwind CSS compiler
  • React Hook Form: a form builder and validation library using React hooks
  • React Modal: an accessible modal component
  • Axios: a promise-based HTTP client for browsers
  • JSON Server: a full, fake REST API server

To set up the React Query Demo application on your machine, execute the following instructions:

# Clone the project
git clone git@github.com:sitepoint-editors/react-query-demo.git

# Navigate to project directory
cd react-query-demo

# Install package dependencies
npm install

# Setup database file for `json-server`
cp api/sample.db.json api/db.json

# Start the `json-server`
npm run json-server

The database file used by json-server contains an array of users. When you execute npm run json-server, a fake API server is launched on port 3004.  Performing a GET request will yield the following sample JSON response:

[
  {
    "id": 1,
    "first_name": "Siffre",
    "last_name": "Timm",
    "email": "stimmes0@nasa.govz",
    "gender": "Male"
  },
  {
    "id": 2,
    "first_name": "Fonzie",
    "last_name": "Coggen",
    "email": "fcoggen1@weather.com",
    "gender": "Female"
  },
  {
    "id": 3,
    "first_name": "Shell",
    "last_name": "Kos",
    "email": "skos2@prweb.com",
    "gender": "Female"
  }
]

Next, start up the dev server that will run the front-end code:

# In another terminal, start the React dev server
npm run dev

Head over to your browser and open http://localhost:3000 to access the application. You should have an identical experience as shown in the preview above. Ensure you perform the following tasks in order to explore the application’s features thoroughly:

  • Review the Basic Query page (home page).
  • Visit the Paginated page and interact with the Previous and Next buttons
  • Visit the Infinite page and interact with the Load more button.
  • Go back to the Basic Query page and click the Create User button. You’ll be directed to the Create User page. Fill in the form and click the Save button.
  • On the User Table, locate the Edit icon. Click on it. This will take you to the Edit User page. Make any changes you like, then hit the Save button.
  • On the User Table, locate the Delete icon. Click on it. This will launch a modal dialog asking you to confirm your delete action. Click on the Delete button to to confirm.

Once we’ve completed all the above tasks, we can start doing a breakdown of the project. Do review the project structure so that you know where each component and view is located. I’ll be providing stripped-down versions of these throughout the article, so that you can understand the fundamentals of using React Query in your projects.

Note: stripped-down versions have classNames, local state and other UI components removed that aren’t the focus of the subject being discussed.

Installing React Query

React Query can be installed on a blank or existing React project using the following command:

npm install react-query

The package comes with everything you need — including the Devtools utility feature, which we’ll explore at a later section. After installing the package, you’ll need to update your top-most component, — App.jsx — as follows:

import { QueryClient, QueryClientProvider } from "react-query";

function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      /* place application containers/views here */
    </QueryClientProvider>
  );
}

export default App;

Any child component of QueryClientProvider will be able to access hooks provided by React Query library. The hooks we’ll be using in this article are:

Here’s an updated (simplified) version of App.jsx containing the child views that we’ll be using:

import { QueryClient, QueryClientProvider } from "react-query";

import BasicQuery from "./views/BasicQuery";
import InfiniteQuery from "./views/InfiniteQuery";
import PaginatedQuery from "./views/PaginatedQuery";
import CreateUser from "./views/CreateUser";
import EditUser from "./views/EditUser";

function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <Switch>
        <Route path="/" exact>
          <BasicQuery />
        </Route>
        <Route path="/paginated">
          <PaginatedQuery />
        </Route>
        <Route path="/infinite">
          <InfiniteQuery />
        </Route>
        <Route path="/user/create">
          <CreateUser />
        </Route>
        <Route path="/user/edit/:id">
          <EditUser />
        </Route>
      </Switch>
    </QueryClientProvider>
  );
}

export default App;

UI Components

Before we proceed to the next section, I think it’s best to have an overview of the main UI components used in the project to display, create and update user data. We’ll start with components/UserTable.jsx. This is table component displays user data and is used by BasicQuery.jsx and PaginatedQuery.jsx pages. It requires one prop, an array of users. Below is a stripped-down version of the completed file:

import React, { useState, useContext } from "react";
import { Link } from "react-router-dom";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";

function UserTable({ users }) {
  const rows = users.map((user, index) => (
    <tr key={index}>
      <td>{user.id}</td>
      <td>{user.first_name}</td>
      <td>{user.last_name}</td>
      <td>{user.email}</td>
      <td>{user.gender}</td>
      <td>
        <Link to={`/user/edit/${user.id}`}>
          <EditIcon />
        </Link>
        <button onClick={() => showDeleteModal(user.id)}>
          <DeleteIcon />
        </button>
      </td>
    </tr>
  ));

  return (
    <React.Fragment>
      <div>
        <Link to="/user/create">Create User</Link>
      </div>
      <table>
        <thead>
          <tr>
            <th>Id</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
            <th>Gender</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    </React.Fragment>
  );
}

Next, we’ll look at components/UserForm.jsx. This form component is used by views/CreateUser.jsx and views/EditUser.jsx pages to perform their tasks. Below is a simplified version of the component:

import React from "react";
import { useForm } from "react-hook-form";
import { useHistory } from "react-router-dom";

import "./form.css";

function UserForm({ user, submitText, submitAction }) {
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm({
    defaultValues: user || {},
  });

  const history = useHistory();

  return (
    <div>
      <form onSubmit={handleSubmit(submitAction)}>
        {user && (
          <section className="field">
            <label htmlFor="id">User Id</label>
            <input type="text" name="id" value={user.id} disabled />
          </section>
        )}

        <section className="field">
          <div>
            <label htmlFor="first_name">First Name</label>
            <input
              type="text"
              {...register("first_name", { required: true })}
            />
            <span className="errors">
              {errors.first_name && "First name is required"}
            </span>
          </div>
          <div>
            <label htmlFor="last_name">Last Name</label>
            <input type="text" {...register("last_name", { required: true })} />
            <span className="errors">
              {errors.last_name && "Last name is required"}
            </span>
          </div>
        </section>

        <section className="field">
          <label htmlFor="email">Email</label>
          <input
            type="email"
            {...register("email", { required: true, pattern: /^\S+@\S+$/i })}
          />
          <span className="errors">
            {errors.email &&
              errors.email.type === "required" &&
              "Email is required"}
            {errors.email &&
              errors.email.type === "pattern" &&
              "Provide a valid email address"}
          </span>
        </section>

        <section className="field">
          <label htmlFor="gender">Gender</label>
          <select {...register("gender", { required: true })}>
            <option value=""></option>
            <option value="Male">Male</option>
            <option value="Female">Female</option>
          </select>
          <span className="errors">
            {errors.gender && "Gender is required"}
          </span>
        </section>

        <div>
          <button type="submit"> {submitText} </button>
          <button type="button" onClick={() => history.goBack()}>
            Back
          </button>
        </div>
      </form>
    </div>
  );
}

export default UserForm;

The UserForm component is designed to perform validation on submitted user data. It expects the following props:

  • user: data object (optional)
  • submitText: text value for the Submit button
  • submitAction: function handling form submission

The user form

In the next section, we’ll start looking at React Query’s main features.

Basic Query

Fetching data using React Query is quite simple. All you need to do is define a fetch function and then pass it as a parameter to the useQuery mutation. You can see an example of views/BasicQuery.jsx page below:

import React from "react";
import { useQuery } from "react-query";

import UserTable from "../components/UserTable";

function BasicQuery() {
  const fetchAllUsers = async () =>
    await (await fetch("http://localhost:3004/users")).json();

  const { data, error, status } = useQuery("users", fetchAllUsers);

  return (
    <div>
      <h2>Basic Query Example</h2>
      <div>
        {status === "error" && <div>{error.message}</div>}

        {status === "loading" && <div>Loading...</div>}

        {status === "success" && <UserTable users={data} />}
      </div>
    </div>
  );
}

export default BasicQuery;

Let’s break it down:

  1. First, we import useQuery via the statement import { useQuery } from "react-query".
  2. Next, we declare a promise function — fetchAllUsers — which will fetch data from our fake JSON API server.
  3. Next, we initiate the useQuery hook function. The following parameters are required:
    • a query key, which can either be a String or an array. It’s used to identify and keep track of query results for caching purposes.
    • a query function, which must return a promise that will either resolve data or throw an error.
  4. The useQuery function returns the following state variables:
    • data: this is the result from the fetch (promise) function.
    • error: if an error is thrown, this will be set. Otherwise it’s null if the fetch request is successful.
    • status: this is a string that can have the value idle, loading, error or success.

The useQuery hook accepts a lot more parameters and returns a lot more variables, which have been documented in the React Query docs. The example above is meant to demonstrate the minimum setup required to perform an API request using the library.

Also, notice how the status variable is reactive. It is initially set to loading. Then, when the request is successful, it’s set to success, causing React to re-render the component and update the UI.

Querying a Single Record

Querying a single record can be achieved using a similar syntax that has been used in the previous section. The difference here is that:

  • you need to pass an argument to the fetch function via an anonymous function
  • you need a unique query name for each individual record, which you can do using an array: [queryName, {params}]
function() {
   const fetchUser = async (id) =>
    await (await fetch(`http://localhost:3004/users/${id}`)).json();

  const { data, error, status } = useQuery(["user", { id }], (id) =>
    fetchUser(id)
  );

  return (...)
}

However, there’s an alternative way of passing arguments. Consider the following code:

const { data, error, status } = useQuery(["user", { id }], fetchUser);

Using the above syntax, you’ll need to modify the fetchUser function to accept a queryKey object as follows:

const fetchUser = async ({ queryKey }) => {
  const [_key, { id }] = queryKey;
  const response = await fetch(`http://localhost:3004/users/${id}`);

  if (!response.ok) {
    throw new Error(response.statusText);
  }

  return response.json();
};

Since we’re using the Fetch API, 404 responses are not considered errors. That’s why we need to write additional logic to handle this situation. Performing this additional check isn’t required when using the Axios API client library.

Check out views/EditUser.jsx to see how the entire code has been implemented. There’s some mutation code in there which we’ll discuss later in the article.

Devtools

Debugging React Query code can easily be done using Devtools. This is a utility that visualizes the inner workings of React Query in real time as your application code executes. Setting it up is as follows:

import { ReactQueryDevtools } from "react-query/devtools";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

When you run your application, there’ll be an icon on the bottom left corner that you can click to expand the Devtools panel.

The Devtools panel

As you can see in the screenshot above, there’s a number of properties you can observe to help you debug your application. In the next section, we’ll look at a couple of configuration options to help you make sense of some these properties and actions.

Configuration

In a React Query application, when a page loads the first time, the library will fetch the data from the API, present it to you and then cache it. You’ll notice a “loading” message when this happens.

When you visit this page a second time, React Query will first return data from the cache and then perform a fetch in the background. Once the asynchronous process is complete, it updates the data on the page. You won’t see a “loading” message at all when this happens.

Caching allows your front-end application to become snappy, especially if you have a slow API server. However, it can present a situation where users may start working with outdated data. In React Query, this is referred to as stale data.

There’s a couple of configuration options that can help you optimize your application for performance or reliability:

  • cacheTime: the default is 5 minutes or 300000 milliseconds
  • staleTime: defaults to 0 milliseconds

cacheTime determines how long data can be stored in the cache before discarding it. staleTime determines how long it takes for data to become outdated. When data becomes stale, it’s marked for re-fetch. This will happen the next time a user visits the page again or re-focuses the browser window/tab.

Increasing the staleTime value can increase application performance if you know that the data being fetched has a low probability of getting updated. You can define these settings by passing a third argument to the useQuery hook:

 function Todos() {

   const result = useQuery('todos', () => fetch('/todos'), {
     staleTime: 60 * 1000 // 1 minute
     cacheTime: 60 * 1000 * 10 // 10 minutes
   })

 }

You can also set Infinity on either property. This will disable garbage collection for cacheTime and make the data never go stale for staleTime.

Paginated Queries

In the Basic Query example, all 250 records were loaded all at once. A more user-friendly approach is to paginate the data. We can achieve this using the useQuery hook. In previous versions of React Query, this was done using the usePaginateQuery hook, which is no longer available in React Query 3.

Pagination implementation actually starts with the back-end API server. Lucky for us, json-server does have pagination support. To access this feature, you need to append the following parameters to an endpoint’s URL:

  • _page: page number
  • _limit: number of records per page

Example: http://localhost:3004/users?_page=5&_limit=10.

Let’s now look at how pagination is achieved using useQuery hook:

import React, { useState } from "react";
import { useQuery } from "react-query";

const pageLimit = 15;

const fetchUsers = async (page = 1) =>
  await (
    await fetch(`http://localhost:3004/users?_page=${page}&_limit=${pageLimit}`)
  ).json();

function Users() {
  const [page, setPage] = useState(1);
  const { data } = useQuery(["paginatedUsers", page], () => fetchUsers(page), {
    keepPreviousData: true,
  });
}

This example is quite similar to the Basic Query we looked at earlier. However, there’s a few key differences:

  1. The promise function, fetchUsers, now accepts an integer page parameter. Page size is set using the variable pageLimit.
  2. The useQuery hook signature looks quite different:
    • The first parameter is an array, ["paginatedUsers", page]. This is to keep track of each page data separately.
    • The second parameter is an anonymous function. It’s defined this way in order to pass the page argument to the fetchUsers function.
    • The third argument is an object config where we can pass multiple settings. In this case, setting the keepPreviousData property to true informs React Query to cache previously fetched data. By default, this setting is false, which causes previous viewed pages to refresh.

To further improve page navigation performance, you can prefetch the next page before the user navigates to it. Here’s an example:

import { useQuery, useQueryClient } from "react-query";

function Example() {
  const queryClient = useQueryClient();
  const [page, setPage] = React.useState(0);

  // Prefetch the next page!
  React.useEffect(() => {
    if (data?.hasMore) {
      queryClient.prefetchQuery(["paginatedUsers", page + 1], () =>
        fetchUsers(page + 1)
      );
    }
  }, [data, page, queryClient]);
}

Take note that data.hasMore is a server API specific property. Unfortunately, our fake API server doesn’t support this. When using a real API back-end, you’d probably get a response that looks something like this:

{
  "items": [
    {
      "lives": 9,
      "type": "tabby",
      "name": "Bobby"
    },
    {
      "lives": 2,
      "type": "Ginger",
      "name": "Garfield"
    },
    ...
  ],
  "meta": {
    "itemCount": 10,
    "totalItems": 20,
    "itemsPerPage": 10,
    "totalPages": 5,
    "currentPage": 2
  },
  "links" : {
    "first": "http://cats.com/cats?limit=10",
    "previous": "http://cats.com/cats?page=1&limit=10",
    "next": "http://cats.com/cats?page=3&limit=10",
    "last": "http://cats.com/cats?page=5&limit=10"
  }
}

Notice that there’s additional metadata provided in the response body structure that can help validate pagination buttons. With json-server, performing a paginated request gives us the following output:

HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Total-Count: 250
Access-Control-Expose-Headers: X-Total-Count, Link
Link: <http://localhost:3004/users?_page=1&_limit=10>; rel="first", <http://localhost:3004/users?_page=4&_limit=10>; rel="prev", <http://localhost:3004/users?_page=6&_limit=10>; rel="next", <http://localhost:3004/users?_page=25&_limit=10>; rel="last"
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"567-FwlexqEes6H/+Xt0qULv2G4aUN4"
Content-Encoding: gzip
Date: Thu, 29 Apr 2021 15:24:58 GMT
Connection: close
Transfer-Encoding: chunked

[
  {
    "id": 42,
    "first_name": "Whitby",
    "last_name": "Damrell",
    "email": "wdamrell15@i2i.jp",
    "gender": "Female"
  },
  {
    "id": 43,
    "first_name": "Fairleigh",
    "last_name": "Staner",
    "email": "fstaner16@tripod.com",
    "gender": "Female"
  },
  ...
]

Take note of the Link attribute provided in the header section. We can use this information to write better pagination code. Unfortunately, the data is not in a format that can readily be used with JavaScript code. We’ll look into how to handle this in the next section. For now, we’ll just use a simple check to determine if we’ve reached the last page.

Below is a stripped-down version of the final views/PaginatedQuery.jsx page:

import React, { useState } from "react";
import { useQuery } from "react-query";

import UserTable from "../components/UserTable";

const pageLimit = 15;

const fetchUsers = async (page = 1) => {
  const response = await fetch(
    `http://localhost:3004/users?_page=${page}&_limit=${pageLimit}`
  );
  return response.json();
};

function PaginatedQuery() {
  const [page, setPage] = useState(1);
  const { data, isLoading, isError, status, error } = useQuery(
    ["paginatedUsers", page],
    () => fetchUsers(page),
    {
      keepPreviousData: true,
    }
  );

  const prevPage = () => {
    if (page > 1) setPage(page - 1);
  };

  const nextPage = () => {
    setPage(page + 1);
  };

  return (
    <div>
      <h2>Paginated Query Example</h2>
      <div>
        {isError && <div>{error.message}</div>}

        {isLoading && <div>Loading...</div>}

        {status === "success" && <UserTable users={data} />}
      </div>

      {/* start of pagination buttons */}
      <div>
        <button onClick={prevPage} disabled={page <= 1}>
          Prev
        </button>
        <span>Page: {page}</span>
        <button onClick={nextPage} disabled={data && data.length < pageLimit}>
          Next
        </button>
      </div>
      {/* end of pagination buttons */}
    </div>
  );
}

export default PaginatedQuery;

In the code example above, we’ve added functions and buttons to provide pagination interaction. Take note that we’re also using isLoading and isError states, which are simply convenient alternatives to using the status state.

Below is a screenshot of the PaginatedQuery page.

A paginated query

Infinite Queries

So far, we’ve only used the useQuery hook to manage data fetching from our back-end API. In this section, you’ll learn how to implement the “infinite scroll” feature. Users will be required to click a Load more button to trigger a data fetch.

To achieve this, we’ll use the useInfiniteQuery hook, which is quite similar to useQuery hook but has several key differences. First, you’ll need a back-end API that supports cursor pagination:

fetch("/api/projects?cursor=0");

Unfortunately, our json-server back end doesn’t. For our purposes, we’ll implement a workaround using the existing pagination support to make infinite querying work. Let’s look at how we define our fetchUsers function:

const pageLimit = 5;

const fetchUsers = ({ pageParam = 1 }) =>
  axios.get(
    `http://localhost:3004/users?_page=${pageParam}&_limit=${pageLimit}`
  );

The function fetchUsers is similar to PaginatedQuery‘s version, except that we’re returning a full Response object instead of a resolved data array. We did this so that we can have access to the Link object provided in the header:

Link: <http://localhost:3004/users?_page=1&_limit=10>; rel="first",
<http://localhost:3004/users?_page=2&_limit=10>; rel="next",
<http://localhost:3004/users?_page=25&_limit=10>; rel="last"

The Link header returns a string that contains meta data about the current position of a page. When using Axios, we can access the above information using response.headers.link. When using the Fetch API to make the request, use response.headers.get('Link') to access the same.

Next, we need to convert the Link metadata into a format that we can easily access in code. We can perform the conversion using this function documented on Josh Frank’s article:

const parseLinkHeader = (linkHeader) => {
  const linkHeadersArray = linkHeader
    .split(", ")
    .map((header) => header.split("; "));
  const linkHeadersMap = linkHeadersArray.map((header) => {
    const thisHeaderRel = header[1].replace(/"/g, "").replace("rel=", "");
    const thisHeaderUrl = header[0].slice(1, -1);
    return [thisHeaderRel, thisHeaderUrl];
  });
  return Object.fromEntries(linkHeadersMap);
};

When we pass the Link‘s header string into the function, we receive the following JavaScript object:

{
  first: "http://localhost:3004/users?_page=1&_limit=5",
  next: "http://localhost:3004/users?_page=2&_limit=5",
  last: "http://localhost:3004/users?_page=50&_limit=5"
}

Now we can extract the value for the next page by using the URLSearch function. You’ll need to supply a partial URL in the format ?_page=2&_limit=5 for it to work. Here’s the snippet of code where we extract the nextPage value:

const nextPageUrl = parseLinkHeader(response.headers.link)["next"];
// split URL string
const queryString = nextPageUrl.substring(
  nextPageUrl.indexOf("?"),
  nextPageUrl.length
); // returns '?_page=2&_limit=5'
const urlParams = new URLSearchParams(queryString);
const nextPage = urlParams.get("_page"); // returns 2

Using the code we’ve defined so far, we now have a “cursor” feature workaround for our fake API back end. You’ll probably have an easier time with a real API back end that supports cursor pagination. With that logic in place, this is how we can define our useInfiniteQuery:

const {
  data,
  error,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  status,
} = useInfiniteQuery("infiniteUsers", fetchUsers, {
  getNextPageParam: (lastPage) => {
    // The following code block is specific to json-server api
    const nextPageUrl = parseLinkHeader(lastPage.headers.link)["next"];
    if (nextPageUrl) {
      const queryString = nextPageUrl.substring(
        nextPageUrl.indexOf("?"),
        nextPageUrl.length
      );
      const urlParams = new URLSearchParams(queryString);
      const nextPage = urlParams.get("_page");
      return nextPage;
    } else {
      return undefined;
    }
  },
});

The above code snippet looks complicated, so let me clarify the useInfiniteQuery syntax for you:

const { ... } = useInfiniteQuery(queryKey, queryFn, {...options})

The are only three arguments that we’re required to provide:

  • The first argument is the queryKey.
  • The second argument — queryFn — is the promise function that fetches cursor paginated data.
  • The third argument is a config JavaScript object, where you define options such as staleTime and cacheTime.

In the case of useInfiniteQuery, you must provide a function called getNextPageParam in order for the infinite scroll button to work. This function determines the next page to load. It has the following syntax:

{
  getNextPageParam: (lastPage, allPages) => {
    // lastPage: the last page(in our case last `Response` object) fetched by `fetchUsers` function
    // allPages: List of all pages that have already been fetched
    // return int|undefined : return `nextPage` as integer. Return `undefined` when there are no more pages
  };
}

Do read the comments to understand the purpose of the function’s inputs and outputs. Let’s now look at the extra states that the hook returns:

  • data: returns an array of pages, data.pages[]
  • fetchNextPage: when this function is executed, it loads the next page, relying on the getNextPageParam function to work
  • hasNextPage: returns true if there’s a next page
  • isFetchingNextPage: returns true while fetching the next page with fetchNextPage

Below is a snippet of how the returned states are used to define our Load more button:

<button
  onClick={() => fetchNextPage()}
  disabled={!hasNextPage || isFetchingNextPage}
>
  Load More...
</button>

With the data.pages[] array, each page item is an array containing data records. Each time a user clicks on the Load more button, a new page item is appended to data.pages[] array. We need to define a new function for extracting records from this nested structure. Take note that in this case, each page is an Axios Response object, so we need to specify page.data to access each user record.

Below is the code snippet that we’ll use to map each user to a <li> tag:

userList = data.pages.map((page, index) => (
  <React.Fragment key={index}>
    {page.data.map((user) => (
      <li key={user.id}>
        {user.id}. {user.first_name} {user.last_name}
      </li>
    ))}
  </React.Fragment>
));

By now, you should have a fundamental understanding of how to use the useInfiniteQuery hook. Let’s now see how the entire views/InfiniteQuery.jsx looks:

import React from "react";
import { useInfiniteQuery } from "react-query";
import axios from "axios";

function InfiniteQuery() {
  const pageLimit = 5;

  const fetchUsers = ({ pageParam = 1 }) =>
    axios.get(
      `http://localhost:3004/users?_page=${pageParam}&_limit=${pageLimit}`
    );

  const parseLinkHeader = (linkHeader) => {
    const linkHeadersArray = linkHeader
      .split(", ")
      .map((header) => header.split("; "));
    const linkHeadersMap = linkHeadersArray.map((header) => {
      const thisHeaderRel = header[1].replace(/"/g, "").replace("rel=", "");
      const thisHeaderUrl = header[0].slice(1, -1);
      return [thisHeaderRel, thisHeaderUrl];
    });
    return Object.fromEntries(linkHeadersMap);
  };

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery("infiniteUsers", fetchUsers, {
    getNextPageParam: (lastPage) => {
      // The following code block is specific to json-server api
      const nextPageUrl = parseLinkHeader(lastPage.headers.link)["next"];
      if (nextPageUrl) {
        const queryString = nextPageUrl.substring(
          nextPageUrl.indexOf("?"),
          nextPageUrl.length
        );
        const urlParams = new URLSearchParams(queryString);
        const nextPage = urlParams.get("_page");
        return nextPage;
      } else {
        return undefined;
      }
    },
  });

  let userList;

  if (data) {
    userList = data.pages.map((page, index) => (
      <React.Fragment key={index}>
        {page.data.map((user) => (
          <li key={user.id}>
            {user.id}. {user.first_name} {user.last_name}
          </li>
        ))}
      </React.Fragment>
    ));
  }

  return (
    <div>
      <h2>Infinite Query</h2>
      <div>
        {error && <div>An error occurred: {error.message}</div>}

        {isFetchingNextPage && <div>Fetching Next Page...</div>}

        {status === "success" && <ul className="my-8 ml-4">{userList}</ul>}
      </div>
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          Load More...
        </button>
      </div>
    </div>
  );
}

export default InfiniteQuery;

Hopefully, the completed code should make sense by now as all the sections have been explained. Below is a screenshot of the “Infinite Query Example” page. I’ve truncated db.json to 13 users to demonstrate the results below:

Infinite query page

Take note that the Load more button is disabled, as we’ve reached the last page. This marks the end of our exploration with query hooks. Let’s look at how we can achieve CRUD functionality using the React Query library.

Mutations

So far, we’ve learned different ways to query data. In this section, you’ll learn how to create, update and delete data using the useMutation hook.

The useMutation hook only requires a promise function that will post data to the back-end API. It will return the following states:

  • isLoading: returns true while the asynchronous operation is running
  • isError: returns true if an error has occurred
  • error: returns an error object if present
  • isSuccess: returns true after the mutation becomes successful

In order to perform the actual mutation action, all you have to do is execute mutation.mutate(data). You can enclose it as a function and assign it to a button’s click event.

Below is a snapshot of the views/CreateUser.jsx page. You can see how each state variable has been used to render various UI elements.

import { useMutation } from "react-query";
import axios from "axios";
import { Redirect } from "react-router-dom";
import UserForm from "../components/UserForm";

const postUser = async (newUser) =>
  await (await axios.post("http://localhost:3004/users", newUser)).data;

function CreateUser() {
  const mutation = useMutation((newUser) => postUser(newUser));
  const { isLoading, isError, error, isSuccess } = mutation;

  const onSubmit = async (data) => {
    mutation.mutate(data);
  };

  if (isSuccess) {
    return <Redirect to="/" />;
  }

  return (
    <div>
      <h2>New User</h2>

      {isError && <div>An error occurred: {error.message}</div>}

      {isLoading && <div>Loading...</div>}

      <UserForm submitText="Create" submitAction={onSubmit} />
    </div>
  );
}

Mutation for the update and delete actions is similar. The only difference is the promise function you provide and the arguments required.

An update mutation example:

const mutation = useMutation((updatedUser) =>
  axios.put(`http://localhost:3004/users/${id}`, updatedUser)
);

A delete mutation example:

const deleteMutation = useMutation((id) =>
  axios.delete(`http://localhost:3004/users/${id}`)
);

If your mutation code is running on the page where your query data is being displayed, you’ll notice that nothing changes after making a commit. In order to trigger a data re-fetch after a successful mutation, you’ll need to execute the queryClient.invalidateQueries() function. See the example below on where to call it:

import { useMutation, useQueryClient } from "react-query";

function UserTable() {
  const deleteMutation = useMutation(
    (id) => axios.delete(`http://localhost:3004/users/${id}`),
    {
      onSuccess: () => {
        queryClient.invalidateQueries();
      },
    }
  );
}

Check out the full reference doc for useMutation to learn about all the states and functions it supports.

Summary

To conclude, React Query is an excellent server state management library for simplifying your data-fetching needs. There are many more features we haven’t looked at that can help you implement even more advance use cases. If you’re wondering if there are any other alternatives to React Query, you should check out their comparison guide for a detailed view of what features each library offers.

FAQs About React Query 3

What is React Query for?

React Query is a versatile JavaScript library designed to enhance data management within React applications. Its primary purpose revolves around simplifying and optimizing data fetching and state management. React Query equips developers with a set of hooks and utilities, such as useQuery and useMutation, to streamline data fetching, caching, and synchronization with various data sources, including APIs and databases.
One of its core functionalities is efficient data caching and retrieval. React Query manages in-memory caching by default, reducing redundant network requests and significantly enhancing application performance. It also simplifies server state synchronization, enabling seamless real-time updates as users interact with the application. This means that when data changes on the client-side, React Query can automatically manage the communication with the server, update the cache, and reflect these changes in the user interface without requiring manual intervention.
Additionally, React Query excels in handling complex data scenarios, such as pagination, infinite scrolling, and optimistic updates. It simplifies the implementation of these features, enhancing the user experience and maintaining consistent application state. With its ability to manage dependencies and perform background data synchronization, React Query empowers developers to create data-rich and responsive React applications with ease, making it a valuable tool for modern web development.

When should you not use React Query?

React Query is a powerful tool for data management in React applications, but there are scenarios where its use may not be the best fit. First, for projects with straightforward data requirements or very minimal data fetching needs, React Query’s feature set might be more than what is necessary, potentially adding unnecessary complexity to the codebase. In such cases, simpler solutions like using the native fetch API or basic React state management may be more appropriate.
Second, if your development team is already proficient in another data management library or pattern, adopting React Query could introduce a learning curve and might not provide significant advantages that outweigh the effort required for adoption. Existing state management solutions, such as Redux or Mobx, might be well-suited to your project, and introducing React Query could lead to redundancy and complexity.
Finally, React Query is primarily designed for client-server interactions, making it exceptionally valuable in applications that interact heavily with APIs or databases. However, for applications relying on serverless or static data sources, the benefits of React Query may not be fully realized. In such cases, a more tailored approach or alternative library might be a more suitable choice.

How do you get query data in React Query?

In React Query, accessing query data is straightforward and efficient through the use of the useQuery hook. First, you define a query by providing a unique query key and a function for fetching data. This definition typically occurs within your React component. The query key helps React Query keep track of the data and manage caching effectively. The fetch function performs the task of fetching data, which can be an asynchronous operation, such as an API call or a database query.
Once the query is defined and executed using useQuery, you gain access to essential properties returned by the hook. These properties include data, which holds the fetched data when the query is successful, error to capture any errors during fetching, and isLoading to indicate whether the query is still in progress. These properties empower you to conditionally render components or display loading indicators and error messages based on the query’s state. React Query also provides isError and isSuccess properties, enabling precise control over your UI based on query outcomes.
With this approach, React Query simplifies data fetching and management in React apps, making it easier to handle loading and error states gracefully while ensuring efficient data caching and synchronization.

How do I fetch data from API using React Query?

Fetching data from an API using React Query is a streamlined process that enhances data management in React applications. It begins by defining a query function responsible for making the API request. This function can use JavaScript’s built-in fetch or other data-fetching libraries like Axios. The query function should return the data to be retrieved from the API.
The heart of data fetching in React Query lies in the useQuery hook. It allows you to execute the defined query function by providing a unique query key and the fetch function. The hook handles the intricacies of data retrieval, caching, and error handling automatically. It returns crucial properties, such as data for the fetched data, error to capture potential errors, and isLoading to determine if the query is still in progress.

What can I use instead of React Query?

There are several alternatives in thr React ecosystem to React Query for data management in React apps. If your project’s data fetching needs are straightforward and you prefer manual control, libraries like Axios or the native Fetch API provide a hands-on approach to fetching data. However, these options lack built-in features like caching and query management that React Query offers, requiring more manual implementation.
For those already invested in Redux for state management, continuing to use it for data management can be a seamless choice. Redux provides a predictable state container that can handle both UI state and fetched data. Integrating Redux Thunk or Redux Saga middleware allows for asynchronous data fetching within a Redux-driven architecture.
On the other hand, if your application relies heavily on GraphQL APIs, Apollo Client is a robust option. It offers comprehensive tools for data fetching, caching, and real-time updates, tailored specifically for GraphQL-based projects. Apollo Client’s integration with React and its focus on GraphQL make it a powerful choice in such scenarios.

Michael WanyoikeMichael Wanyoike
View Author

I write clean, readable and modular code. I love learning new technologies that bring efficiencies and increased productivity to my workflow.

fetch datafetching dataReactReact QueryReact Query 3
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week