Fetch data in an effective way with React Query

In this article, we will talk about the React Query library and how it can make your React apps more fast and responsive. Web applications are becoming very popular nowadays, and everyone wants to build a good app for their customers. But when your applications start to make many API calls, it can be cumbersome to guarantee a nice and fluid experience to your users.

That being said, where should we begin? Well, React Query is a library that can help you to create hooks to fetch data. Wait a minute, do I need that? Fetching data is one of the most trivial things to a web app. The web is asynchronous, and we are familiar with the task of fetching data aren’t we?
You probably have seen some code like this:

import {useState, useEffect} from "react";

type Status = "idle" | "loading" | "error";

const usePosts = () => {
    const [status, setStatus] = useState<Status>("idle");
    const [error, setError] = useState(false);
    const [posts, setPosts] = useState<Post[]>([]);

    useEffect(() => {
        setStatus("loading")
        get<Post[]>("/posts").then(res => {
            setStatus("idle");
            setPosts(res.data);
        }).catch(err => {
            setStatus("error");
            setError(err)
        });
    });

    return {
        posts,
        isLoading: status === "loading",
        isError: status === "error"
        error
    };
}

It seems pretty straightforward. Doesn’t it? So why do I need a library to do that? The first problem with this code is that you probably will repeat it many times throughout your codebase. The first thing that caught my attention about React query is that it provides you a nice hook that does everything that we’re doing on the code above and much more.
So, the first benefit of using React Query is that it will reduce dramatically the amount of code necessary to perform a request.

Well, you could argue that you could create a reusable hook for that, and then you would no longer need another dependency in your project, right? In this case, you probably would be forcing yourself to reinvent the wheel, but just for fun, we could try to do something like this:

const useRequest = (request) => {
    const [status, setStatus] = useState<Status>("idle");
    const [error, setError] = useState(false);
    const [data, setData] = useState<Post[]>([]);

    useEffect(() => {
        setStatus("loading")
        request().then(res => {
            setStatus("idle");
            setData(res.data);
        }).catch(err => {
            setStatus("error");
            setError(err)
        });
    });

    return {
        data,
        isLoading: status === "loading",
        isError: status === "error"
        error
    };
}

// Now everytime that you need to do a request you could use the useRequest
const usePost = () => {
    return useRequest(() => get<Post[]>("/posts"))
}

Isn’t that nice? You simplified your requests without installing an extra dependency. This code already simplifies your requests, but what about caching? What if I need this data in another place in the application? And this is where React Query shines, it not only provides you with a hook to simplify your requests, but also gives you a set of other features to deal with server state such as caching, deduping requests, and more.

So let’ talk more about React Query and what it can offer to our projects.

Simplify the way that you fetch data with useQuery

As I mentioned a few paragraphs before, you’re probably familiar with the logic necessary to fetch data. React hooks have simplified this a lot, and now we can use a bunch of useEffect‘s and useState‘s to help us solve this problem. But you probably will repeat the same code every time you need to perform a request in your app. You could create something like the useRequest hook that was provided in the example above, but React Query already comes with a solution out of the box, the best part is that its use is very similar to the hook we wrote above, except that it has much more happening under the hood.

So, to your requests, you could use React Query’s useQuery, which requires only two arguments to work. The first one is a unique key, and the second is a function that returns a promise that can resolve with some data or rejects if something goes wrong.
This is how we could write our usePosts hook:

import { get } from 'api';

const usePosts = () => useQuery("posts", () => get("/posts"));

// Then in your component
const {isLoading, isError, data, error} = usePosts();

See, very simple isn’t it? But, you should be asking yourself, why do we need a key, and this leads us to the next topic.

Caching

React Query can cache the results of your requests and use this cache to deliver a better experience for your users, with fewer loadings and requests. When using the useQuery hook, you have to specify a unique key on its first parameter. React Query uses this key to manage the cache.

The cache feature works with the stale while revalidates strategy. So it means that users will see a loading screen only the first time the app makes the request. Then they will see the old data while the app tries to revalidate (update) the current data. This strategy is simple but has its elegance while makes a huge difference in the usability and performance of the app. Users will be happier, having a more responsive app, giving them the feeling that everything works very fluidly.

Keep your UI always up to date with useMutation and caching invalidation

While querying data (read/GET requests), you will be fine using the useQuery hook. But the documentation encourages us to use the useMutation hook anytime you are performing some server side-effects or changing some data on the server (create, update, delete).

The best part is that useMutation can invalidate the cache, and the lib will update the invalid data on its own. Take a look at the code below:

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

const getComments = newComment => axios.post("/comments").then(res => res.data);

const postComments = newComment => axios.post("/comments", newComment)
    .then(res => res.data);

export const useComments = () => useQuery("comments", getComments);

export const useMutateComments = () => {
    const queryClient = useQueryClient();

    return useMutate(postComments, {
        onSuccess: () => queryClient.invalidateQueries("comments")
    });
}

// to use the useMutateComments hook.

// It is very similiar to the useQuery
const {isLoding, isError, mutate} = useMutateComments();

// to post a new comment
const handlePostNewComment = comment => {
    mutate(comment);
}

In this example, we have a list of comments, and the useComments hook is using the useQuery, to fetch all comments from the API. We also have a useMutateComments that is responsible for creating a new comment.

So, in this case, every time that I post a new comment, my comment list should update. As we are invalidating the cache to the "comments" key on the mutation, React Query is going to mark the request as stale. If any component, using the useComments hook is being rendered, React Query will trigger a request to revalidate the data instantaneously.

This way, you can keep the UI always up to date.

Say goodby to your global state

Not really, but you certainly will reduce the amount of data that you consider global. React Query handles the state differently, separating the user state from the server state. Maybe you will continue storing things like themes and user preferences globally. But there is no need to keep with the results of your API calls on the global state anymore.

React Query deduplicates API calls, so if two components are being rendered and both perform the same API call (make sure they are using the same key), then instead of making the request twice, it will make it once and share the results among these components.

Maybe, you already have tried to solve this problem by making the API call from a top-level component. Then you shared the data using Redux, Context API, Recoil, or another state management tool.

That’s why this lib can be an alternative to these global state management tools. As long the queries have the same key, React Query will provide the appropriate data "globally" for every piece that needs so.

Also, If components A and B use the same data and your app renders component A first, then when the time comes to render component B, it will not display any loadings. It will use the data that is already in cache while revalidates the data in the background. If the data changes, both of the components will then be updated. Isn’t that cool?

Retries: Because things can go wrong eventually

Another thing that works out of the box is the query retries. We know that eventually, something can go wrong, and the API can not respond for a few seconds. In this situation, React Query will try to call API again.

So, whenever the query function throws an error, React Query will automatically retry the query a few times before displaying the error to the user. You can configure the max number of retries, but the default is 3.

Conclusion

React Query is a library that can help you with fetching and caching data, causing a huge, positive impact on the usability of your apps. You can also look at other similar libraries such as SWR from Vercel.
I expect I could have shown you all the good parts of React Query. The idea was, to sum up, all the things that caught my attention and share a little bit of knowledge. Still, the best place to learn about it’s by their docs.
The lib has much more features, including support to GraphQL. So if you are interested, you should check out their docs. I’m sure you are going to find something that can help you with your next project.

Thanks for reading up here, and have a happy coding!

We want to work with you. Check out our "What We Do" section!