How did I solve data fetching and caching complexity with React Query

Igor Zanella
4 min readMay 2, 2023

Data fetching and caching have always been a pain in React applications. Every time you write a lot of boilerplate code, you must maintain it and maybe rewrite it for every API request you want to add.

We also had this problem in Customerly with the messenger app, where we created a custom framework to manage API requests results and cache them in some Redux states.

We then had to develop the web application, so we started from the bottom and searched the libraries that fit our needs. We found Tanstack Query.

Tanstack Query

Tanstack Query

What is Tanstack Query? Tanstack Query (also called React Query) is a library that simplifies the state management of data you can get from API requests.

This library:

  • Caches data.
  • Dedupes multiple requests into one.
  • Updates “out of date” data in the background.
  • Permits pagination or infinite queries.
  • Memoizing cached data.
  • Manages memory and garbage collection of server state.
  • And much more…

Is it easy to implement?

Implementing React Query is very easy. You can find how to do it on the docs pages.

How did we implement it?

We initially created a folder called network, but let’s see how we implemented the hooks in our code.

We divided the network folder into different subfolders for every section of our API. Then we created a file for every “entity” of our API.

Let’s see the useEmailApi.ts example. Obviously, the entity is email. Now we want to see the request where we get a single email from our API.

const EMAILS_KEY = "emails";

const useGet = (
emailId?: number,
options?: Omit<UseQueryOptions<Email, AxiosError>, "queryKey" | "queryFn">
) => {
const appId = useRecoilValue(currentAppIdState);

return useAuthenticatedQuery<Email, AxiosError>(
[EMAILS_KEY, appId, emailId],
() =>
client.get({
path: `/v3/emails/${emailId}`,
params: { app_id: appId },
}),
{
staleTime: Infinity,
enabled: !!emailId,
...options,
}
);
};

As you can see from the code, we are creating a new hook called useGet . If you don’t call this hook with the use prefix, React will return an error because you use a hook inside a “normal” function.

Inside this hook, we call a Recoil value to get the appId . I will show you what Recoil is in another article.

Then we call useAuthenticatedQuery , a custom hook we created to check if the user is authenticated. As you can see, we have 3 parameters in this hook.

The first is the query key, which we need to set a unique id for caching. This will be useful when we want to invalidate the query. In this case, we have 3 values that make this array unique. The first one is a constant string. Then there are the appId and the emailId, which are the values that actually make the array unique.

The second one is the client. We created a custom function with Axios to create requests like in the example simply.

The third parameter is the options object, where we set staleTime to Infinity because we don’t want the query to invalidate automatically. Then we set enabled to call the query only if we emailId is not undefined.

With this simple hook, the result is already cached, and if we call the hook in a different part of the code, it won’t recall the API because of the staleTime: Infinity, otherwise, it uses the cached data, but revalidate the query and updates it.

Finally, at the end of the file, we export that hook with a default declaration like the one below, with all the other hooks.

export default {
get: useGet,
getRendering: useGetRendering,
getAll: useGetAll,
getInsights: useGetInsights,
getUsers: useGetUsers,
getTopClicks: useGetTopLinks,
getTopUsersSeen: useGetUsersTopSeen,
getTopUsersClick: useGetUsersTopClick,
post: usePost,
postDuplicate: usePostDuplicate,
postTest: usePostTest,
put: usePut,
delete: useDelete,
};

How did we use it?

We now go into UI files to see the usage of the hook we created.

const { data: email, isLoading: isEmailLoading } = useEmailApi.get(
Number(emailIdParam)
);

As you can see, it’s simple. We imported the whole useEmailApi file, then we called it with .get(). This is because sometimes we need other APIs in the same file, and we don’t need to import other functions.

The hook returns different values, like data, the actual data of the API request, but also the isLoading value, which we can use to do some UIX behavior.

Let’s see an example of usage.

<Title level={"h3"}>
{isEmailLoading ? (
<Skeleton width={300} />
) : (
email && (
<>
{email.name}
{email.type === EmailType.OneShot && (
<EmailStatusLabel
status={email.status}
scheduled={isScheduled}
/>
)}
</>
)
)}
</Title>

As you can see, we show a Skeleton component if the email is loading, then we check if the email value exists, and we show some data.

What about POST, PUT, and DELETE requests?

For POST and PUT requests, we used another hook which is useMutation, but we are not seeing it in this article.

Do you have other examples or tutorials?

Yes, I’ve just created my second course on Udemy. So, if you want to master React Query library and three more libraries, check it out.

Mastering React Libraries course on Udemy

The course is Mastering React Libraries (with Customerly codebase). Exactly, you will see the Customerly production codebase in the course. I’ve also added a 30% discount for you in the link above.

Conclusion

React Query saved us a lot of time because we didn’t lose time writing boilerplate code and we don’t need to maintain custom code. Contact me if you need some help. You can also find me on igorzanella.dev, on LinkedIn, or on Twitter.

--

--

Igor Zanella

Full Stack Developer in Customerly | Freelance Developer | Tech Entrepreneur