Using Redux Tool Kit (RTK) Query With React Router V6
Starting out with some new technology is always challenging and overwhelming. You usually get involved in a big sea of articles and personal opinions. It causes you trouble and makes you fall into a confusing situation and leaves you bewildered.
The same thing happened to me when I started working on RTK Query using React Router v6.
In order to get yourself aligned with what you really need to do and what the exact technology is, read this blog post and you will get an understanding of the technology before starting work on it.
If you are a developer and working with different technologies, this blog post will help you use RTK Query with React Router v6.
Table of Contents
- What is RTK Query?
- The Learning Curve
- The motivation behind RTK Query
- How to use RTK Query with axiosBaseQuery?
- AxiosBaseQuery
- APIS File with CreateAPI and baseQuery
- Dispatching Contact List Loader
- Listing Contact
- Fetching a Single Contact
- Showing the List of Contacts & Single Contact through Single Loader
- Base Loader
- Loading Root Data
- How to Create, Update and Delete a Contact using RTK through the Actions and Loaders in the app?
- Using React Router v6
- Code for Deleting the Contact
- Favorite Contact
- Navigation Process Between Loaders and Actions
- Conclusion
In this blog post, we have tried to make it as simple as it could be. It covers all the basic knowledge bases for React Router 6 with RTK Query. For reference, we used the Official Tutorial from React Router Documentation. Without further ado, let’s get started with the most exciting and informative blog post:
What is RTK Query?
RTK Query is considered a powerful tool used for caching and data fetching. It helps in simplifying the common cases to load data in a web application while eliminating the requirement of writing logic for data fetching and caching.
The Learning Curve
I used the React Router v6 in front-end using RTK Query and created a db.json file to run on JSON-server. If we want to make the API Requests like (POST , PUT, DELETE , PATCH) changes will be automatically saved to the db.json using lowdb. We have to make sure that the request must contain Content-Type: application/json.
The motivation behind RTK Query
RTK is an advanced data-fetching technology that helps you out with client-side caching. It is similar to React Query but it has the edge of being directly integrated with Redux. By RTK Query, you can have the flexibility of the API interactions instead of using them with async middleware modules like thunks. In this article, I’ll tell you how you can use RTK query along with the React Router v6 (Loaders and Actions). You can see the implementation for the Actions and Loaders of React Router v6 here. I’ll just tell you how you can efficiently use React Router v6 and RTK query together.
How to use RTK Query with axiosBaseQuery?
Before going into details, we will first define the AxiosBaseQuery.
AxiosBaseQuery
This is known as the default method for handling the queries through the option of baseQuery on createApi, combined with the option of query on an endpoint definition.
console.log( 'import axios from "axios";
import { BASE_URL } from "../config/app.config";
export const axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
"cache-control": "no-cache",
},
});
export const axiosBaseQuery =
({ baseUrl } = { baseUrl: "" }) =>
async ({ url = "", method, data, params }, { signal }) => {
try {
const result = await axiosInstance({
url: baseUrl + url,
method,
data,
params,
signal,
});
return { data: result.data };
} catch (axiosError) {
let err = axiosError;
return {
error: {
status: err.response?.status,
data: err.response?.data || err.message,
},
};
}
};
Definition of APIs with CreateAPI and baseQuery
Now, let’s discuss the api’s file where all the apis are defined with createAPI and baseQuery.
console.log(import { createApi } from "@reduxjs/toolkit/query/react";
import { axiosBaseQuery } from "./base.api";
export const contactsApi = createApi({
baseQuery: axiosBaseQuery({
baseUrl: "/contacts",
}),
tagTypes: ["Contacts"],
endpoints: (build) => ({
getContactList: build.query({
query: (q) => ({ url: `?${q ? `q=${q}` : ""}` }),
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: "Contacts",
id,
})),
{ type: "Contacts", id: "LIST" },
]
: [{ type: "Contacts", id: "LIST" }],
}),
getContact: build.query({
query: (id) => ({ url: `/${id}` }),
providesTags: (result, error, id) => [{ type: "Contacts", id }],
}),
deleteContact: build.mutation({
query: (id) => ({ url: `/${id}`, method: "DELETE" }),
invalidatesTags: [{ type: "Contacts", id: "LIST" }],
}),
createContact: build.mutation({
query: (data) => ({ url: "/", method: "post", data }),
invalidatesTags: (result, error, arg) => [
{ type: "Contacts", id: arg.id },
{ type: "Contacts", id: "List" },
],
}),
updateContact: build.mutation({
query: (data) => ({
url: `/${data.id}`,
method: "put",
data,
}),
invalidatesTags: (result, error, arg) => [
{ type: "Contacts", id: arg.id },
],
}),
}),
});
export const { useGetContactListQuery, useGetContactQuery } = contactsApi;
Dispatching Contact List Loader
Below is the code snippet for the loader that we have for the contact listing where we dispatch the API interaction functionality initially. By this, you can control the behavior of the RTK Query when it re-fetch all subscribed queries when it regains the connection. You can also abort the signal calls afterward and then return the data. The listing will be shown on the sidebar and it will be updated with every other change in the data.
console.log(export const contactListLoader =
(dispatch) =>
async ({ request }) => {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const promise = dispatch(
contactsApi.endpoints.getContactList.initiate(q)
);
request.signal.onabort = promise.abort;
const res = await promise;
const { data: contacts, isError, isLoading, isSuccess } = res;
return { contacts, q };
};
Listing Contact
In the contact listing, we are getting the URL from searchParams and we have updated data. This will return the promise and we assign the promise.abort to the onabort() coming from the signal. Then, we, abort the request with a signal if we are visiting another page it will cancel the call.
Fetching a Single Contact
Below is the code for fetching a single contact. Its loader has the same sort of data as in the above contact listing loader. But in this, we also return an isError flag and error message if we visit some wrong Id.
console.log(export const contactLoader =
(dispatch) =>
async ({ params, request }) => {
const promise = dispatch(
contactsApi.endpoints.getContact.initiate(params.contactId)
);
request.signal.onabort = promise.abort;
const res = await promise;
const { data: contact, isError, error, isLoading, isSuccess } = res;
console.log(res);
if (isError) {
const { status = 403, data } = error;
throw new Response("", {
status,
statusText: data?.message || "Contact not found",
});
}
return contact;
};
Showing the List of Contacts & Single Contact through Single Root Loader
We have made this code more generic because taking the above code we have to define all the loaders individually for all the required calls. Now below is the code snippet for the main loaders for Showing the List of contacts and showing a single contact through a single loader that we can use for all the loader calls.
console.log(export class ContactsLoader extends BaseLoader {
listLoader = async ({ request }) => {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const { data: contacts } = await this._loader(
contactsApi.endpoints.getContactList,
request,
q
);
return { contacts, q };
};
detailLoader = async ({ params, request }) => {
const { data: contact } = await this._loader(
contactsApi.endpoints.getContact,
request,
params.contactId
);
It is just like the old way of doing things in react but basically, this is the efficient way if you want to use all your loaders handled from a single method. Make a single class and call both loaders from there and that will just take the required params and requests.
Exporting Base Loader
Now, we will discuss the Base Loader. It’s actually a Root Loader that will trigger all the other loaders like (Contact Listing and Single Contact) Loader. Below is the code snippet for BaseLoader().
console.log(export class BaseLoader {
_store = {};
_dispatch = () => {};
constructor(store) {
this._store = store;
this._dispatch = store.dispatch;
}
_loader = async (endpoint, request, query, queryOptions) => {
const promise = this._store.dispatch(
endpoint.initiate(query, queryOptions)
);
request.signal.onabort = promise.abort;
const res = await promise;
const { data, isError, error } = res;
if (isError) {
const { status = 403, data } = error;
throw new Response("", {
status,
statusText: data?.message || getErrorMessage(status),
});
}
return data;
};
}
Loading Root Data
We initialized the _store and _dispatch in a constructor and loaded the root data in _loader using endpoint.initiate(). We also handled the errors at the bottom of the code so that if I am unable to find a contact then this must throw an error from the root which can save us from writing a whole lot of code for all the loaders like (Listing and fetching a single contact).
Here is the detailed view of the base.loader.js file.
console.log(const CLIENT_ERROR = "Bad request";
const SERVER_ERROR = "Server error";
const UNKNOWN_ERROR = "Something went wrong";
const NOT_FOUND = "Contact not found";
const NONE = "";
export const getErrorMessage = (status = 403) => {
if (!status) return UNKNOWN_ERROR;
if (status < 300) return NONE;
if (status === 404 && status < 500) return NOT_FOUND;
if (status === 400 && status < 500) return CLIENT_ERROR;
if (status >= 500) return SERVER_ERROR;
return UNKNOWN_ERROR;
};
export class BaseLoader {
_store = {};
_dispatch = () => {};
constructor(store) {
this._store = store;
this._dispatch = store.dispatch;
}
_loader = async (endpoint, request, query, queryOptions) => {
const promise = this._store.dispatch(
endpoint.initiate(query, queryOptions)
);
request.signal.onabort = promise.abort;
const res = await promise;
const { data, isError, error } = res;
if (isError) {
const { status = 403, data } = error;
throw new Response("", {
status,
statusText: data?.message || getErrorMessage(status),
});
}
return data;
};
}
Other than the root loader, we are just defining the error messages with the provided status codes.
How to Create, Update and Delete a Contact using RTK through the Actions and Loaders in the app?
Mutations are used to send data updates to the server and apply the changes to the local cache. Now talking about the above code, how Mutation can help is that it re-fetches it and data validation occurs. The method createApi() returns an object in the endpoints section. We then define the fields with build.mutation(). You can see that in the above contactsApi snippet.
Using React Router v6
Now, how we handle this with my React Router v6 actions. We are showing you that in the form of snippet. You just need to follow the below code snippets.
Firstly, this is the action for creating the contact. For further details about actions in React Router v6, See Here. In this example, we are just making a formdata and upon getting the entries with Object.fromEntries we then dispatch the createContact with initiate().In editContactAction, we just send the id and the updated data.
console.log(export const createContactAction =
(dispatch) =>
async ({ request }) => {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await dispatch(contactsApi.endpoints.createContact.initiate(updates));
return redirect(`/contacts`);
};
In editContactAction, we just send the id and the updated data.
console.log(export const createContactAction =
(dispatch) =>
async ({ request }) => {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await dispatch(contactsApi.endpoints.createContact.initiate(updates));
return redirect(`/contacts`);
};
export const editContactAction =
(dispatch) =>
async ({ params, request }) => {
const formData = await request.formData();
let updates = Object.fromEntries(formData);
updates = { ...updates, id: params.contactId };
await dispatch(contactsApi.endpoints.updateContact.initiate(updates));
return redirect(`/contacts/${params.contactId}`);
};
How to Delete the Contact?
Below is the deleteContactAction for deleting a contact in which I am dispatching the deleteContact with initiate() and sending the contactId.
console.log(export const deleteContactAction =
(dispatch) =>
async ({ params }) => {
await dispatch(
contactsApi.endpoints.deleteContact.initiate(params.contactId)
);
return redirect("/");
};
Finally, I have a very interesting action that marks the favorite contacts.
export const toggleFavContactAction =
(state) =>
async ({ request, params }) => {
const { data: prevData } = contactsApi.endpoints.getContact.select(
params.contactId
)(state.getState());
let formData = await request.formData();
return await state.dispatch(
contactsApi.endpoints.updateContact.initiate({
...(prevData || {}),
favorite: formData.get("favorite") === "true",
id: params.contactId,
})
);
};
How to Add Favorite Contacts?
Finally, we have a very interesting action that marks favorite contacts.
console.log(function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
return (
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite ? "Remove from favorites" : "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</fetcher.Form>
);
}
In this flow, we are using .select() that returns a new selector function instance. You can get more information on this from here. We are also using useFetcher() which will make the navigation processes easy between the loaders and actions. You can see the code snippet below.
Conclusion:
Well wrapping it all, it seems that it is really flexible and worth implementing. It makes my app more efficient and more productive. The API interaction with the RTK query shows effective outcomes.
I hope this blog post will help you in developing your app using RTK Query with React Router v6.
You can find the complete code here https://github.com/Bitsol-Technologies/react-contacts-app
If you have any suggestions or feedback, do share them in the comment section. For more detail, please visit our website: https://bitsol.tech/