Over 10 years we help companies reach their financial and branding goals. Engitech is a values-driven technology agency dedicated.

Gallery

Contacts

411 University St, Seattle, USA

engitech@oceanthemes.net

+1 -800-456-478-23

Using React 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 and selecting not a single option. The same thing happened to me when I started working on RTK Query using React Router v6. RTK Query is considered a powerful tool used for coaching 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 coaching. 

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. 

In this blog post, I have tried to make this guide as simple as it could be. This blog post covers all the basic knowledge bases for React Router 6 with RTK Query. For reference, I used the Official Tutorial from React Router Documentation. Without further ado, let’s get started with the most exciting and informative blog post:

The Learning Curve: 

I used the React Router v6 on the 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, and 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:

An RTK query that helps you out with client-side caching and it is an advanced data-fetching technology. 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, I will first define the 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. 

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,

         },

       };

     }

   };

Now, let’s discuss the api’s file where all the apis are defined with createAPI and baseQuery.

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;

Below is the code for the loader that I have initially 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.

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 };

    };

In the contact listing, I am getting the URL from searchParams and I have updated data. This will return the promise and I assign the promise.abort to the onabort() coming from the signal. Then, I, abort the request with a signal if I am visiting another page it will cancel the call.  

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, I also return an isError flag and error message if I visit some wrong Id.  

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;

    };

I have made this code more generic because taking the above code I have to define all the loaders individually for all the required calls. Now below is the code snap for the main loaders for Showing the List of contacts and showing a single contact through a single loader that I can use for all the loader calls. 

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. 

Now, I 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().

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;

    };

}

I just initialized the _store and _dispatch in a constructor and load the root data in _loader using endpoint.initiate(). I 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.

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, I am 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. I then define the fields with build.mutation(). You can see that in the above contactsApi snap code. 

Now, how I handle this with my React Router v6 actions. I am showing you that in the form of snapcode. 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().

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, I just send the id and the updated data. 

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}`);

    };

Below is the deleteContactAction for deleting a contact in which I am dispatching the deleteContact with initiate() and sending the contactId.

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,

            })

        );

    };

In this i am using .select() that returns a new selector function instance. You can get more information on this from here. I am also using useFetcher() which will make the navigation processes easy between the loaders and actions. You can see the code snippet below.

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>

    );

}

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. 

If you have any suggestions or feedback, do share them in the comment section. For more detail, please visit our website: https://bitsol.tech/

Recent Posts

Subscribe to get notifications for Latest Blogs