Guide to Using Redux Toolkit Query Mutation

Today, we are going to learn how to add data using Redux Toolkit. In Redux Toolkit Query terms, changing data is called a mutation, and reading data is called a query. We will understand how to implement this and the logic behind it. We will also explore the tag system that Redux Toolkit Query uses. So, without further ado, let's get started.

Project Setup

We will have a single button for adding data to keep things simple. We will display the data we fetch, and when "Add Data" is clicked, we will run a mutation to add data to the display. The data will come from a library called Faker, and the backend will be a JSON server. However, you can apply these concepts using your own API because the principles remain the same.

npm create vite@latest
npm i

Create a project using Vite or your preferred tool to set up a React project. After setting up and installing the dependencies, you will need these packages for your Redux Toolkit Query setup.

npm install @reduxjs/toolkit
npm install react-redux
npm install --save-dev @faker-js/faker
npm i json-server

Setting up JSON Server

Inside your package.json add a script called start:server with value npx json-server db.json. Inside your root project create a db.json.

// package.json
...
"scripts": {
    "start:server": "npx json-server db.json", // Add this...
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  }
...
// db.json
{
  "users": [
    {
      "name": "Kumar",
      "id": "1"
    }
  ]
}

Test if your server is set up correctly by running npm run start:server.

Creating Redux Store and usersApi for RTK Query

Create a new folder inside your src directory called store. Inside this, create a folder named apis, and within apis, create a file called usersApi.js. All the queries or mutations will be consolidated here.

// src/store/apis/usersApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

const usersApi = createApi({
  reducerPath: "users",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:3000",
  }),
  endpoints(builder) {
    return {
      fetchUsers: builder.query({
        query: () => {
          return {
            url: "/users",
            method: "GET",
          };
        },
      }),
    };
  },
});

export const { useFetchUsersQuery } = usersApi;
export { usersApi };

I discussed this setup in detail in my previous blog post, which you can view here:
https://shiwanshudev.hashnode.dev/how-to-set-up-redux-toolkit-query

In summary, we imported createApi from Redux Toolkit. createApi takes an object with three main properties: reducerPath, baseQuery, and endpoints.

The reducerPath is the name used inside the store. The baseQuery takes an object with a baseUrl property, which is your API's base URL. Lastly, endpoints is where we define all the mutations and queries for our usersApi.

The endpoint is a function that takes builder as an argument. We need to return an object with keys for various operations. The fetchUsers property is defined for fetching users. Since we are reading data, we use query here. For a mutation, as we will see later, we use builder.mutation. We return an object from builder.query with the query property, which is the URL configuration for making the request. The url property will be appended to the baseUrl we defined earlier. It might seem confusing at first, but you will get used to the pattern.

Be very careful when importing createApi and baseQuery. Often, the "react" at the end gets left out in the import statement if you use your IDE's autocomplete feature.

Setting up our Redux store

// src/store/index.js
import { configureStore } from "@reduxjs/toolkit";
import { usersApi } from "./apis/usersApi";
import { setupListeners } from "@reduxjs/toolkit/query";

const store = configureStore({
  reducer: {
    [usersApi.reducerPath]: usersApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(usersApi.middleware),
});

setupListeners(store.dispatch);
export { store };
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { Provider } from "react-redux";
import { store } from "./store/index.js";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

Inside the store file located at src/store/index.js, we configure our reducers by using the [usersApi.reducerPath] property as usersApi.reducer. This means we evaluate usersApi.reducerPath and use it as a key for usersApi.reducer. The [] syntax does not create an array. In configureStore, we also add our middleware and finally export the store.

Inside src/main.tsx, we set up the Provider and pass it the store we exported earlier.

Using the Redux Toolkit Query to fetch data

// src/App.jsx
import "./App.css";
import { useFetchUsersQuery } from "./store/apis/usersApi";

function App() {
  const { data, isLoading, error } = useFetchUsersQuery();
  console.log(data);
  const handleAddUser = () => {};
  return (
    <div className="container">
      <button className="btn" onClick={handleAddUser}>
        Add Users
      </button>
      <div className="users">
        {data &&
          data.map((user) => {
            return <div className="user">{user.name}</div>;
          })}
      </div>
    </div>
  );
}

export default App;

Inside the App.jsx file located in the src folder, we call useFetchUsersQuery(), which we can destructure into data, isLoading, and error. We use this data to display our users. Ensure the JSON server is running with npm run start:server, and then start your app with npm run dev.

Adding a User with Mutation

// src/store/apis/usersApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

const usersApi = createApi({
  reducerPath: "users",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:3000",
  }),
  endpoints(builder) {
    return {
      fetchUsers: builder.query({
        query: () => {
          return {
            url: "/users",
            method: "GET",
          };
        },
        providesTags: ["User"], // ADD THIS
      }),
      addUser: builder.mutation({
        query: (user) => {
          return {
            url: "/users",
            method: "POST",
            body: {
              name: user.name,
              id: user.id,
            },
          };
        },
        invalidatesTags: ["User"], // ADD THIS
      }),
    };
  },
});

export const { useFetchUsersQuery, useAddUserMutation } = usersApi;
export { usersApi };

Similar to how we defined fetchUsers inside the endpoints return object, we define another property called addUser. This will use builder.mutation instead of builder.query because we are modifying data. Note that the object it takes has a query property again; it is more like a set of configurations for the URL, method, etc., so don't confuse it with the builder.mutation property. Since we are creating a user, we set the method to POST and the URL to "/users" because that is what we defined in our db.json earlier. The body will be the data we pass into this query from our App.jsx.

ProvidesTags and InvalidatesTags Property:

The tags are essentially used to enable automatic data refetching. What does this mean? If you remove both providesTags and invalidatesTags, you will notice that even though the data appears in console.log when printed in App.jsx, the view will not update. To prevent this, we associate a tag with each request. If the URL changes, we need to use a function call that also changes according to the URL for this to work correctly. For more information, refer to the official documentation below.

Official Documentation:
https://redux-toolkit.js.org/rtk-query/usage/automated-refetching

Using the mutation inside our App

// src/App.jsx
import "./App.css";
import { useAddUserMutation, useFetchUsersQuery } from "./store/apis/usersApi";
import { faker } from "@faker-js/faker";

function App() {
  const { data, isLoading, error } = useFetchUsersQuery();
  const [addUser, results] = useAddUserMutation();
  console.log(data);
  const handleAddUser = () => {
    addUser({
      name: faker.internet.userName(),
      id: faker.string.uuid(),
    });
  };
  return (
    <div className="container">
      <button className="btn" onClick={handleAddUser}>
        Add Users
      </button>
      <div className="users">
        {data &&
          data.map((user) => {
            return (
              <div key={user.id} className="user">
                {user.name}
              </div>
            );
          })}
      </div>
    </div>
  );
}

export default App;

Inside our App.jsx, we call the useAddUserMutation function exported from usersApi.js. The line const [addUser, results] = useAddUserMutation(); sets up our addUser mutation. Unlike the query above it, this line returns an array. We destructure it to use addUser with the object we want to send as the body of the POST request.

...
  const handleAddUser = () => {
    addUser({
      name: faker.internet.userName(),
      id: faker.string.uuid(),
    });
  };
...

Here, as you can see, we are using faker to generate a random username and ID, which we then pass to addUser. This initiates the POST request. We attach the handleAddUser function to the button we created to trigger this action. Finally, we run the app with the server running to make this request.

Thank you for reading this. If you have any questions, feel free to reach out to me.

1
Subscribe to my newsletter

Read articles from Shiwanshu Shubham directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shiwanshu Shubham
Shiwanshu Shubham

Hey there, I'm Shiwanshu, a passionate frontend developer and designer hailing from India. With proficiency in NextJS, ReactJS, CSS3, HTML5, Figma, UI Design, and Typescript, I've embarked on a journey into the tech space. This blog is a documentation of my progress as I delve deeper into the world of technology.