RTK Query - Redux Toolkit Query tutorial

In this post you will learn what is RTK Query or Redux Toolkit Query and how it can solve your needs.

What is RTK Query?

The first question is what is RTK Query? This is an additional package inside Redux Toolkit library which allows us to simplify fetching data.

If you are familiar with React Query or Apollo Client it is going in the similar direction.

RTK Query simplifies fetching data, caching it, invalidating and reusing.

In this post we won't talk about Redux Toolkit itself. If you don't have enough knowledge about Redux Toolkit and maybe have just knowledge about plain Redux make sure to check this post first.

Real example

To understand RTK Query let's look on my example. Here I already prepared a small project.

Initial project

Here we load a list of users from API and we can add a new user, send a POST request and it will be saved in database.

So here we make 2 API calls: to get users and to create a user. Let's look on the code.

// src/features/users/Users.js
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getUsersAsync, createUserAsync } from "./usersSlice";

const Users = () => {
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = useState("");
  const users = useSelector((state) => state.users.data);
  const addUser = () => {
    dispatch(createUserAsync(inputValue));
    setInputValue("");
  };

  useEffect(() => {
    dispatch(getUsersAsync());
  }, [dispatch]);

  return (
    <div>
      <div>Total: {users.length}</div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={addUser}>Add user</button>
      </div>
      <div>
        {users.map((user, index) => (
          <div key={index}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};
export default Users;

This is our components where we load a list of users and create new user. As you can see we use here Redux Toolkit a lot like dispatch(getUsersAsync()) for example.

// src/features/users/usersSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getUsers, createUser } from "./usersApi";

export const getUsersAsync = createAsyncThunk("users/getUsers", async () => {
  const response = await getUsers();
  return response.data;
});

export const createUserAsync = createAsyncThunk(
  "users/createUser",
  async (name) => {
    const response = await createUser(name);
    return response.data;
  }
);

const initialState = {
  data: [],
  isLoading: false,
  error: null,
};

export const usersSlice = createSlice({
  name: "users",
  initialState,
  extraReducers: (builder) => {
    builder
      .addCase(getUsersAsync.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getUsersAsync.fulfilled, (state, action) => {
        state.isLoading = false;
        state.data = action.payload;
      })
      .addCase(getUsersAsync.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload;
      })
      .addCase(createUserAsync.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(createUserAsync.fulfilled, (state, action) => {
        state.isLoading = false;
        state.data.push(action.payload);
      })
      .addCase(createUserAsync.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload;
      });
  },
});

export default usersSlice.reducer;

This is our reducer for users. We have here standard stuff like data, isLoading, error. Also we have here getUsersAsync and createUserAsync which creates for us 2 async actions.

This is exactly how we implement working with API without RTK Query

But now we want to refactor this code to use RTK Query. It allows us to write much less code just because we fetch data, make loading indicator and show error message again and again. This is exactly what this library does for us.

  • It tracks loading state
  • It avoids duplicate requests
  • It implements optimistic updates
  • It manages cache for requests

But I don't want to show you dry documentation. Let's convert our RTK application to RTK Query.

RTK Query Configuration

Most important point is that we don't need to install anything. We get RTK Query inside RTK package out of the box but if we don't write it's imports it is removed from our build as unused code.

First we can completely remove our usersSlice.js with all reducer and state logic. It was 53 lines of code that we don't need anymore. So now we have just our component and nothing else.

Now we want to register RTK Query. In order to do that we need a new slice because typically we define a single slice for RTK Query in our application.

// src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const apiSlice = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => "users",
    })
  }),
});

export const { useGetUsersQuery} = apiSlice;

Here we used createApi function in order to generate a slice. First of all we set reducerPath which is just a name of the slice. Then we provided baseQuery. Typically all API requests are coming from a single place this is why it have a lot of sense to set a base query.

After this we have endpoints. It's the most important property as we define our requests here. First request that we need is getUsers to fetch the array. As a value we set builder.query because we make a GET request. Inside it we must define query property where we set what api we must call. In our case it is /users.

Additionaly at the bottom of the file we destructure useGetUsersQuery from our apiSlice. It allows us to work with fetched data directly in our component.

We don't need to do any changes in our Redux setup but we must provide this api slice to our root reducer.

// src/store.js
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./features/api/apiSlice";

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

We don't need to register here users slice anymore so we removed it. Instead we provided here a reducer for an API. Additionaly we added here a middleware fromm RTK Query so it is working correctly.

Now we just need to refactor our Users component.

// src/features/users/Users.js
import { useCreateUserMutation, useGetUsersQuery } from "../api/apiSlice";

const Users = () => {
  const [inputValue, setInputValue] = useState("");
  const { data } = useGetUsersQuery();
  const users = data ?? [];
  const addUser = () => {
    setInputValue("");
  };
  console.log("data", users);

  return (
    <div>
      <div>Total: {users.length}</div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={addUser}>Add user</button>
      </div>
      <div>
        {users.map((user, index) => (
          <div key={index}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};

Here we removed useEffect and users state as we get our data through useGetUsersQuery hook. It returns not only data but also isLoading, isError and much more. Because our data is undefined by default we check that and set an empty array there.

As you can see in browser our code works like previously without any changes.

RKT Query dev tools

Here in the devtools we can see 3 actions which we get from RTK Query. Also in the API slice inside we can see fetched data. Now users data is fetched on initialize and is just available in the component without any problems from our side.

Just look how much code we wrote. We don't need reducer anymore at all and any code regarding data fetching, useEffects or syncing state in the component.

Mutations

But we just did a half of our project. But we didn't implement creation. This is why let's implement a new endpoint to do creation of the user.

export const apiSlice = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
  endpoints: (builder) => ({
    ...
    createUser: builder.mutation({
      query: (name) => ({
        url: "users",
        method: "POST",
        body: { name },
      }),
    }),
  }),
});

export const { useGetUsersQuery, useCreateUserMutation } = apiSlice;

We added createUser here. As we need to provide method and body we return an object and not just a string. Also we get name as an argument to create a user. We will provide it when we call a function in our component. Also we exported new useCreateUserMutation from our apiSlice.

Now let's update our component.

const Users = () => {
  const [inputValue, setInputValue] = useState("");
  const [createUser] = useCreateUserMutation();
  const addUser = () => {
    createUser(inputValue);
    setInputValue("");
  };
  ...
}

We got here useCreateUserMutation which gives us createUser function as a second argument. By calling it we will trigger createUser API call.

As you can see in browser we can add a user, we see actions in redux devtools and network request but our list was not updated.

Caching

This is why it is time to talk about caching. This is exactly what happened. We have cached request for the list of users but our mutation (creating of the user) never changed this cached data.

What we need to do instead is to tell RTK Query to invalidate our users list and refetch at after we add new element.

export const apiSlice = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
  tagTypes: ["Users"],
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => "users",
      providesTags: ["Users"],
    }),
    createUser: builder.mutation({
      query: (name) => ({
        url: "users",
        method: "POST",
        body: { name },
      }),
      invalidatesTags: ["Users"],
    }),
  }),
});

Here we made 2 changes. First of all we added tagTypes. In the whole API slice we can define what entities we have and which request does it. After this we added providesTags in our get request. It means that this request provided Users tag. With that we can now invalidate this type which we do by adding invalidatesTags for createUser.

It means that when we call our createUser mutation our tag Users will be invalidated and data will be automatically refetched.

Invalidation

As you can see RTK Query is a really versatile tool where we are writing much less code.

Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!

📚 Source code of what we've done
Did you like my post? Share it with friends!
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.