Guest Article

Efficient Data Fetching with React Query

Google+ Pinterest LinkedIn Tumblr

This guest post on the Applozic blog was written by Mr. Nitin Ranganath. Nitin is a computer engineering student and an avid full-stack developer who loves to build for the web and mobile. He creates user-centric websites with React, TypeScript, Node.js, and other JavaScript technologies. You can find him on his website, Twitter, Instagram, and GitHub.

Nitin Ranganath

React is one of the most popular JavaScript libraries that developers tend to lean toward for building interactive and scalable web applications. Part of its popularity can be attributed to the unopinionated nature of React and the freedom it offers to the developers.

However, this can also act as a double-edged sword as React does not ship with a built-in data fetching solution. Due to this, more often than not, developers end up implementing their own solution, which can turn out to be inferior to what could be done. 

The Traditional Way of Data Fetching

One of the common patterns that React developers use for data fetching involves cobbling together the component-level and side-effects via the useState() hook and the useEffect() hook. If you’re not familiar with this approach, here’s an example that elucidates it:

import { useState, useEffect } from "react";
import "./styles.css";

export default function App() {
  
  const [data, setData] = useState([]);
  const [loading, setLoading]<code> = useState(false);
  const [error, setError] = useState("");
  
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      try {
        const res = await fetch("https://jsaonplaceholder.typicode.com/users");
        const users = await res.json();
        setData(users);
      } catch (err) {
        setError(err.message);
      }
      setLoading(false);
    };
    fetchUsers();
  }, []);
  
  return (
    <div className="App">
      <h1>Data Fetching</h1>
      <h2>The React Hooks Way</h2>
      <div>
        {loading ? (
          <p>Loading...</p>
        ) : error ? (
          <p>{error}</p>
        ) : (
          data.map((user) => <p key={user.id}>{user.name}</p>)
        )}
      </div>
    </div>
  );
}

CodeSandbox Demo

In the above example, we require 3 component-level states to keep track of the data, loading status, and the error message. On top of that, we call the asynchronous fetch() function inside the effect hook to make a network request to the API and manage the component-level state accordingly.

While there isn’t anything fundamentally wrong with this approach, this is a lot of business logic that clutters the component. Even with this bare minimum example, the lines of code and logic we had to write were significant. Not quite good in terms of reusability!

The other popular approach usually seen in big applications is storing the data in the global state with state management tools like Context API, Redux, or other state management libraries. While this is great for avoiding issues like prop drilling and achieving data communication amongst isolated components, it still doesn’t address the problems that are exclusive to the server state.

And this is where React Query shines. Let’s take a deep dive into it and figure out how we can use it to make data fetching painless. 

An Introduction to React Query 

React Query is a data fetching library that can help you manage asynchronous operations between your server and client and efficiently fetch your data. But that’s not all. It brings a bunch of handy features to improve the user experience and make your React application faster and more responsive. 

Before we discuss the features of React Query, let us first understand the term server state. Server state is the asynchronous data that is fetched from a server or an API. Unlike the client state, the server state has shared ownership and the potential to become stale as soon as you’re done fetching it.

Therefore, managing server state efficiently is a challenge in itself. Here are some of the major features that React Query offers to combat these challenges:

  • Caching the data for future use.
  • Deduping multiple requests for the same data into a single request.
  • Pagination and infinite scrolling.
  • Prefetching the data.
  • Knowing when the data gets outdated and updating it in the background.

And a lot more. Enough talk. Let’s check out how to integrate React Query in your React application.

Adding React Query to Your React Project

To start with React Query, use the following command to install it as an NPM package:

npm install react-query    

# or, if you use yarn:

yarn add react-query   

Before you can start using the hooks from React Query, you will need to import QueryClient and QueryClientProvider from react-query and wrap it around the <App /> component in index.js. This will ensure that all the components in the React application will have access to the hooks and cache.

import { QueryClient, QueryClientProvider } from "react-query";
import ReactDOM from "react-dom";
import App from "./App";

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
 document.getElementById("root")
);

CodeSandbox Demo

And voila, setting up React Query is that simple. Let’s start making actual network requests and fetch data from an API using React Query’s useQuery hook in the next section.

The useQuery() Hook

For our data fetching needs, we’ll be using the useQuery hook from react-query to handle all the server state for us. This is going to be one of the most used hooks of this library.  Let’s use the same example that we used in the traditional data fetching methods section but use React Query this time.

import { useQuery } from "react-query";
import "./styles.css";

export default function App() {

  const fetchUsers = async () => {
    const res = await fetch("https://jsonplaceholder.typicode.com/users");
    const users = await res.json();
    return users;
  };

  const { data, isLoading, isError, error } = useQuery("users", fetchUsers);

  return (
    <div className="App">
      <h1>Data Fetching</h1>
      <h2>With React Query's useQuery Hook</h2>
      {isLoading ? (
        <p>Loading...</p>
      ) : isError ? (
        <p>{error.message}</p>
      ) : (
        data.map((user) => <p key={user.id}>{user.name}</p>)
      )}
    </div>
  );
}

CodeSandbox Demo

The useQuery hook takes two required parameters: the query key (more on this in the upcoming section) and the fetcher function, which returns a promise. Apart from this, you can also pass in an optional object parameter to configure the cache time, stale time, and other properties.

In return, React Query provides an object full of useful information related to the request, such as the status, data, loading state, failure count, and a lot more. In the above example, I’ve destructured the data, isLoading, isError, and the error from the returned object to render the UI accordingly. 

Link to useQuery documentation 

React Query Devtools

React Query comes with an in-built devtools that can be very handy for monitoring and understanding the lifecycles of a request. Here’s how you can import and use it in your React application:

import { QueryClient, QueryClientProvider } from "react-query";
// Importing the devtools from react-query/devtools
import { ReactQueryDevtools } from "react-query/devtools";
import ReactDOM from "react-dom";
import App from "./App";

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools />
  </QueryClientProvider>,
  document.getElementById("root")
);

CodeSandbox Demo

Debugging how React Query works is extremely easy as all the requests you make are logged to the devtools along with the complete information related to it. You can open it by clicking on the floating React Query logo on the bottom left side of your screen.

By default, the devtools is only enabled on the development environment and gets disabled on production, so you don’t have to worry about that. I would highly recommend you to use it for a better and visual understanding.

Query Keys And Its Importance

To make the most out of React Query, it is essential that you understand the query keys and the role it plays in the useQuery() hook. Query keys are unique keys used to identify each request.  Let’s understand this practically with a simple example.

import { useQuery } from "react-query";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Query Keys</h1>
      <h2>Example 1</h2>
      <h3>Users</h3>
      <Users />
      <h3>Posts</h3>
      <Posts />
    </div>
  );
}

function Users() {
  
const { data, isLoading, isError, error } = useQuery("users", () =>
    fetch("https://jsonplaceholder.typicode.com/users").then((res) =>
      res.json()
    )
  );

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

  return (
    <div>
      {data.map((user) => (
        <p key={user.id}>{user.name}</p>
      ))}
    </div>
  );
}

function Posts() {

  const { data, isLoading, isError, error } = useQuery("posts", () =>
    fetch("https://jsonplaceholder.typicode.com/posts?_limit=5").then((res) =>
      res.json()
    )
  );

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

  return (
    <div>
      {data.map((post) => (
        <p key={post.id}>{post.title}</p>
      ))}
    </div>
  );
}

CodeSandbox Demo

In the above example, we have 2 components: <Users /> and <Posts /> to display the users and posts, respectively. However, notice that the query key in the useQuery() hook in both the components are different. If you keep the same query key, both the requests will be considered the same even though we are requesting different resources.

Let us take another example where the query key could be dynamic, such as a prop:

import { useState } from "react";
import { useQuery } from "react-query";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Query Keys</h1>
      <h2>Example 2</h2>
      <Users />
    </div>
  );
}

function Users() {  
  const [id, setId] = useState(null);
  
  const { data, isLoading, isError, error } = useQuery("users", () =>
    fetch("https://jsonplaceholder.typicode.com/users").then((res) =>
      res.json()
    )
  );
  
  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>{error.message}</p>;
  
  return (
    <div>
      {data.map((user) => (
        <p key={user.id} onClick={() => setId(user.id)}>
          {user.name}
        </p>
      ))}
      <br />
      {id && <UserInfo id={id} />}
    </div>
  );
}

function UserInfo({ id }) {
  const { data, isLoading, isError, error } = useQuery(["users", id], () =>
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((res) =>
      res.json()
    )
  );

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

  return (
    <div>
      <h4>{data.name}</h4>
      <h4>{data.email}</h4>
      <h4>{data.phone}</h4>
      <h4>{data.website}</h4>
    </div>
  );
}

CodeSandbox Demo

This example will help you truly understand the purpose of query keys and how it is used to cache the data. The <Users /> component renders a list of all the users and uses the query key “users” for the useQuery() hook. On clicking any of the users from the list, the <UserInfo /> component is rendered with the user’s ID being sent as a prop.

This <UserInfo /> component uses a dynamic query key of [“users”, id] to retrieve the user’s complete information. Since the id is unique for each user, React Query can use it to cache the data and re-fetch it whenever needed. This is a bit similar to the dependency array in the useEffect() hook.

The first time you click on a user from the list, React Query will fetch the details of that user from the API and store it in the cache. Even if you click on some other user and click back on this user, React Query will render the cached data of the user and automatically re-fetch the data in the background. 

Therefore, the content is rendered instantaneously without any loading state. On re-fetching, if any content has been changed, the UI will be re-rendered automatically. This makes your website look performant and provides a better user experience.

The useMutation() Hook

Till now, we have considered scenarios where we’re fetching the data from the server or API when the component is mounted. However, this may not always be the case. For example, we may need to make network request to create, update or delete resources on the server. And this is where the useMutation() hook from react-query comes into play.

As the name suggests, this hook allows you to make requests to your server or API to mutate the state instead of just fetching them. Here’s an example that demonstrates the useMutation()  hook being used to add a new todo:

import { useState } from "react";
import { useMutation } from "react-query";
import "./styles.css";

export default function App() {

  const [todo, setTodo] = useState("");

  const mutation = useMutation(
    () =>
      fetch("https://jsonplaceholder.typicode.com/todos", {
        method: "POST",
        headers: {
          "Content-type": "application/json; charset=UTF-8"
        },
        body: JSON.stringify({
          userId: 1,
          title: todo,
          completed: false
        })
      }).then((res) => res.json()),
    {
      onSuccess(data) {
        console.log("Succesful", { data });
      },
      onError(error) {
        console.log("Failed", { error });
      },
      onSettled() {
        console.log("Mutation completed.");
      }
    }
  );

  async function addTodo(e) {
    e.preventDefault();
    mutation.mutateAsync();
  }

  return (
    <div className="App">
      <h1>useMutations() Hook</h1>
      <h2>Create, update or delete data</h2>
      <h3>Add a new todo</h3>
      <form onSubmit={addTodo}>
        <input
          type="text"
          value={todo}
          onChange={(e) => setTodo(e.target.value)}
        />
        <button>Add todo</button>
      </form>
      {mutation.isLoading && <p>Making request...</p>}
      {mutation.isSuccess && <p>Todo added!</p>}
      {mutation.isError && <p>There was an error!</p>}
    </div>
  );
}

CodeSandbox Demo

In the above example, the useMutation() hook takes a function that makes a POST request to the API endpoint to add a new todo. We can also add some side-effects such as onSuccess, onError, and onSettled to keep track of the mutation’s status.

To run the mutation, call mutateAsync() or mutate() on the mutation object returned by the useMutation() hook. You can similarly make requests for updating and deleting the data on the server.

Link to useMutation Documentation

Other Features to Check Out

But that’s not all. React Query has a lot more features that you can explore and use in your next projects, such as infinite scrolling, pagination, and pre-fetching. Be sure to check out the documentation. I would also highly recommend you to watch some talks from Tanner Linsley, the creator of React Query, to understand how it differs from other data fetching libraries such as SWR. You can also check out Applozic’s React Native Tutorial here.

That is all for this blog post. I hope you understood React Query and would consider using it in your future projects.

Author

Do you want to share your thoughts with the Global App Development Community? Write for Applozic! Check out how here: https://www.applozic.com/guest-writer/