If you're starting your journey as a React developer, learning to manage an application's remote state is crucial unless your application doesn't have a backend.
Before we jump straight into coding, let's learn what exactly a remote state is and why we need a better approach for its management.
Remote State is your application's state saved on a server via backend service or API, i.e, users profile data.
Keeping your React application (Frontend) synced with remote state is crucial for the responsiveness and scalability of your application.
Some of the operations for managing state include caching, syncing state, and data mutation.
Popular third-party packages among developers:
Fortunately, you don't have to do it manually; there are many third-party packages available, for example, React Query, SWR, and RTK Query. All you need to do is install and configure your React application.
React Query is the most loved package among developers over the year due to its simplicity, robustness, optimization, scalability, and active community.
Today, we're going to learn how to use it in your next application, and I guarantee this article will be enough to learn core features.
We will use React Query by Tanstack in our project.
Benefits of React Query:
React Query provides several benefits, including:
- Caching
- Data Mutation
- Re-validation of data
- Automatically updating the expired cache
- Better user experience
- Performance optimizations
- Clean coding
and many more.
Installation:
Let's install React Query in our codebase via the following command line. I'm using NPM (Node Package Manager)as I've been using it for a decade. You can also use npm or yarn.
Using npm:
npm install @tanstack/react-queryUsing pnpm:
pnpm add @tanstack/react-queryDefining The Provider:
After the package installation is done, let's configure React Query according to our project by creating a separate component named "ReactQueryProvider".
This component accepts children props (so we wrap our App component with this provider, more about that later) and returns a QueryClientProvider.
Let's create our Query client by instantiating the QueryClient imported from the tanstack package. It accepts an option object to customize the default behavior of React Query.
I set gcTime to 24 hours in milliseconds; in other words, the cache will expire after 24 hours until it is fetched again.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});
function ReactQueryProvider({ children }: { children: React.ReactNode }) {
return //
}Next, we need to provide the newly created queryClient to QueryClientProvider.
To do so, let's import QueryClientProvider from thetanstackpackage and pass the queryClient object via the client prop.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});
function ReactQueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
export default ReactQueryProvider;Let's wrap the content of the App component with ReactQueryProvider, and we're done with the provider.
Fetching Remote State:
Let's fetch the remote state to see the React Query in action. In this example, I'm using Supabase to store my data; you can use any backend service or API.
// THIS FUCTION RETURNS ARRAY OF SUBSCRIPTIONS
// REACT QUERY MAKES IT EASY TO HANDLE ERRORS AS WELL
export async function getAllSubscriptions() {
const { data: subscriptions, error } = await supabase
.from("subscriptions")
.select("*");
if (error) throw error;
return subscriptions;
}We are creating a custom hook for getting our data.
Please don't worry if you have no idea about custom hooks; we'll also discuss that topic.
Creating a separate file for your custom hook. I usually place my custom hooks under the hooks folder.
Export the function that starts with the "use" keyword, i.e, useSubscriptions, and returns some value.
export function useSubscriptions(){
// CODE FOR HOOK
}Make sure your custom hook function starts with "use" keyword. Otherwise react won't let you use other hooks inside this function.
Now call the useQuery hook from the tanstack package, imported from the tanstack package.
It takes an object of options and returns data along with the request status via a variable named isPending.
import { useQuery } from "@tanstack/react-query";
export function useSubcriptions() {
const { isPending, data: subscriptions } = useQuery({
// OPTIONS OBJECT
});
return { isPending, subscriptions };
}Let's discuss the options object. You can read more about these options in the official documentation. But only 2 options are mandatory:queryKey and queryFn.
queryKey: This is an array of unique keys.
It helps React Query to save the results in cache with these keys. The more elements you add to the array, the unique it would be.
queryFn: This function fetches the data from the server. In my case, I'm fetching subscription data from Supabase.
import { getAllSubscriptions } from "@/services/subscription-api";
import { useQuery } from "@tanstack/react-query";
export function useSubcriptions() {
const { isPending, data: subscriptions } = useQuery({
queryKey: ["subscriptions"],
queryFn: getAllSubscriptions,
});
return { isPending, subscriptions };
}React Query revalidates, caches, and automatically syncs the data, keeping your code clean and easy to understand.
You need to call this custom hook to get subscriptions along with the isPending variable (Fetching state).
function Subscriptions(props){
const {isPending,subscriptions} = useSubscriptions();
return //
}Mutating Remote State:
We learnt how to get the remote state, but what about mutation? Fortunately, React Query provides hooks for mutating data.
We are creating a custom hook for mutating our subscriptions.
Let's take a look at our Supabase function for creating new services.
export async function createSubscription(payload) {
const { data, error } = await supabase
.from("subscriptions")
.insert([payload])
.select();
if (error) throw error;
return data;
}It accepts a payload object (subscription object), inserts it into the Supabase dashboard, and throws an error if any.
Next, create a new file named " useCreateSubscription " in the hooks or features folder. Define a function that starts with the"use" keyword.
Then import useQueryClient and use Mutation from the react-query package.
function useCreateSubscription(){
const queryClient = useQueryClient();
const {mutate:createSubscription, isPending} = useMutation({
// OPTIONS
})
}Our custom hook returns a mutate function and the isPending variable.
For the options object, it accepts a mutation function (createSubscription) via the mutationFn option, along with the onError and onSuccess functions.
- mutationFn: A mutation function that accepts a payload when calling the mutate function returned from useMutation.
- onSuccess: A function that is executed when the mutation is successful, ideal for displaying success messages via toast components.
- onError: A function that is executed when the mutation fails, ideal for displaying errors to users via alerts or toast components.
- queryClient: A client object that offers various features of React Query, including the invalidateQueries method that helps to re-fetch and update the cache using the query key.
The mutationFn option, onError, and onSuccess functions.
import { useToast } from "@/hooks/use-toast";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createSubscription as createSubscriptionApi } from "@/services/subscription-api";
import { useAuth } from "../authentication/useAuth";
export function useCreateSubscription() {
const { toast } = useToast();
const { user } = useAuth();
const queryClient = useQueryClient();
const { mutate: createSubscription, isPending } = useMutation({
mutationFn: (payload: any) =>
createSubscriptionApi({ trainer: user?.id, ...payload }),
onSuccess: () => {
toast({
title: "Plan created!",
description: "Your workout plan is ready for registering clients",
});
queryClient.invalidateQueries({ queryKey: ["subscriptions"] });
},
onError: (err) =>
toast({
title: err.name,
description: err.message,
variant: "destructive",
}),
});
return { createSubscription, isPending };
}Here's the full code for the mutation hook.
Now, let's test this hook and create some side effects in our application.
const {isPending, createSubscription} = useCreateSubscription();
function handleSubmitForm(values){
createSubscription(values, {
onSuccess: () => {
form.reset();
onSuccess();
},
});
}createSubscription accepts a payload (in this case, subscription data necessary for creating a new record in Supabase) and an options object to help you extend the onSuccess or onError functionality.
In my scenario, I'm using the onSuccess method to reset the form after a successful submission.
Conclusions:
In the end, I suggest you use it at least once, and I guarantee you will be surprised by the effort and pain it eliminates for managing your remote state. You need to learn useQuery and useMutation, and it will do 80% of your job.
Thank you so much for reading this article. Please share this article with your friends or family.
About the Author:
Umar is a passionate and self-taught developer. After graduating from Law, he decided to pursue his dream by building web applications and SaaS for himself and businesses seeking to grow online.
He's offering a free consultation as an experienced Frontend Developer (ReactJS and NextJS).
If you want to improve your existing application or start from scratch, please DM Umar Saleem or book your free consultation at Arfa Developers.
If you have any queries or complaints, please email us at umar@arfadev.com.
