Back

Integrating RTK Query with Redux Toolkit

Integrating RTK Query with Redux Toolkit

This article will walk through a step-wise process to integrate RTK Query with Redux Toolkit in a React application, to guide React developers in leverage RTK Query capabilities to enhance the Redux core functionality.

React developers relied heavily on Redux for state management in large applications. However, this approach led to excessive and repetitive code, creating more opportunities for bugs. The Redux team introduced the Redux Toolkit to address these, especially concerns around the Redux core. It provides tools to help React developers simplify their application code. Redux Toolkit also provides tools that extend its functionality beyond state management. These additional use cases include the powerful data-fetching and caching capability provided by RTK Query. This use case eliminates the need for hand-written data fetching and caching logic. Therefore, it allows React developers to manage Redux state and server-side data effectively.

Understanding RTK Query’s Functionality

The Redux Toolkit package provides RTK Query as an optional tool for efficient data management. It offers impressive data fetching and caching capabilities. This feature eliminates the need for hand-written logic. It also adds a unique approach to its application programmable interface (API) design, streamlining its integration. In the coming sub-section, we’ll discuss what makes this tool unique.

Key RTK Query APIs and Core Features

This module supports several APIs for leveraging its data management features, including:

  • createApi(): It represents the RTK Query core functionality. This API accepts a collection of endpoint definitions for communicating with a specific server. The createApi() method also includes configuration for data retrieval and transformation.
  • fetchBaseQuery(): This API simplifies requests by wrapping the fetch API.
  • <ApiProvider />: It can be used as a Provider in the absence of the Redux store.
  • setupListeners(): A utility that supports the refetchOnMount and refetchOnConnect functionalities.

Furthermore, RTK Query also offers several features that improve efficiency and productivity. The following are some of the key features:

  • Streamlined data fetch and caching: It simplifies the use case for managing loading state, data retrieval, and caching. It also provides options for transforming responses, invalidating cache, and re-fetching data. This functionality eliminates manual handling of data fetching and caching.
  • Auto-generated React hooks: It also automatically generates React hooks that encapsulates the entire data fetching process. It manages the lifetime of cached data as components mount and unmount.
  • Optimistic updates: It facilitates optimistic user interface (UI) updates immediately after triggering a mutation. This feature gives the user the impression of immediate changes even though their request is still processing.
  • Polling: This module allows us to run queries at specified intervals. Providing the pollingInterval field to the useQuery hook enables this feature.

Knowing these APIs and features is unsatisfactory; we need a solid understanding for effective application. The next sub-section will discuss how RTK Query operates.

How RTK Query Works

RTK Query operates by first creating an API slice instance using the createApi function. This function manages your endpoints and provides optional configurations for handling requests. Each endpoint definition can be a query or a mutation. By default, RTK Query provides fetchBaseQuery to handle request headers and response parsing. The fetchBaseQuery is often set up with a base uniform resource locator (URL) and header options. This setup ensures each endpoint can leverage it for consistent requests.

Furthermore, RTK Query manages the cached responses by assigning each endpoint a cache key. When attempting a request, it checks if the data is already cached and then serves it. If the data is not cached, a new request gets sent, and the response is stored in the cache. This approach prevents the client side from making unnecessary requests to the server. Finally, the auto-generated hooks manage request status and data update, so components only re-render when necessary.

Step-by-step Utilization of RTK Query in a React Project

To improve our understanding of RTK Query, we’ll build a task manager application that interacts with a server. I have set up a starter repository for a task manager, so we can focus solely on its integration. I have also set up a mock API using mockapi.io. Let’s run the following commands to clone the repository and get started:

git clone https://github.com/chumex412/task-manager-starter
cd task-manager-starter
pnpm install
pnpm add @reduxjs/toolkit react-redux

Below is a visual representation of the task manager starter project:

A simple task manager application

Initializing RTK Query

We already established RTK Query as a module in the Redux Toolkit package. To use it, import createApi and define an API slice instance as shown in the code below:

// src/redux/slice/taskSlice.ts

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

const baseUrl = `https://${import.meta.env.BASE_API_URL_TOKEN}.mockapi.io/api/v1`;

export const tasksApiSlice = createApi({
  reducerPath: "tasksApi",
  baseQuery: fetchBaseQuery({
    baseUrl,
 }),
  endpoints: (build) => ({}),
});

The code above shows a basic configuration of the createApi function, creating an API slice instance. We achieved this configuration by defining the baseQuery and endpoints properties. The baseQuery handles each endpoint’s requests setup and utilizes fetchBaseQuery to simplify request configurations. The endpoints field is currently empty, but that’s about to change soon.

Setting Up a Basic API Query

So far, we have created an API slice instance with basic configurations. Now, let’s define a query to fetch all tasks from the server:

// src/redux/slice/taskSlice.ts

export const tasksApiSlice = createApi({
  // ...previous code here

  // Updated code
  tagTypes: ["Tasks"],
  endpoints: (build) => ({
    getTasks: builder.query<Task[], string | number>({
      query: () => `/tasks`,
      providesTags: ["Tasks"],
 }),
 }),
});

export const { useGetTasksQuery } = tasksApiSlice

The code snippet above includes extra configurations to the endpoints field. It defines a query to fetch all tasks and generates a corresponding query hook. By specifying the tagTypes field as ["Tasks"], we can manage a particular endpoint’s cached data. Also, using providesTags with the same ["Tasks"] label prevents unnecessary task-related data updates.

The next step involves importing and utilizing the useGetTaskQuery to retrieve all tasks. Consider the RenderContent component code update below:

// src/modules/tasks/RenderTaskContent.tsx

const RenderContent = () => {
  const { data: tasks, isLoading, isSuccess } = useGetTasksQuery("");
  // Previous component code logic

  return isLoading ? (
 {/* Loader JSX */}
 ) : isSuccess ? (
 <>
 {/* Previous JSX code */}
 <section className="mt-8">
 <Tasks tasks={filteredTasks || tasks} tab={todoTab} />
 </section>
 </>
 ) : (
 <p className="text-center text-2xl leading-[150%]">Failed to fetch</p>
 );
};

Invoking the useGetTasksQuery hook in the RenderContent component made the task data accessible. But, there’s one problem: our UI is currently blank. The visual aid below shows that the absence of a context value crashed the application:

The errors of the crashed task manager application

// src/App.tsx

import { ApiProvider } from "@reduxjs/toolkit/query/react";
import { tasksApiSlice } from "./redux/slice/taskSlice";
import TaskContainer from "./container/TaskManager";

function App() {
  return (
 <ApiProvider api={tasksApiSlice}>
 <TaskContainer />
 </ApiProvider>
 );
}

We fixed the error by wrapping the TaskContainer component with the ApiProvider from the RTK Query. This approach gives React access to the appropriate context value. The visual representation below shows the UI content after resolving the error:

The application's UI after fixing the error

However, no visible task because we didn’t retrieve any data from the source. In the next sub-section, we will use RTK Query to send mutation requests that add and update tasks.

Performing Create, Read, Update and Delete (CRUD) Operations

This sub-section focuses on using RTK Query to execute CRUD operations. In addition to the query endpoint, we need mutation endpoints to update the task data. Let’s adjust our API slice endpoints to include mutations that enable us to achieve this feat. Consider the following:

// src/redux/slice/taskSlice.ts

export const tasksApiSlice = createApi({
  // Initial setup code
  endpoints: (builder) => ({
    // ...previous code here
    addTask: builder.mutation({
      query: (task: Task) => ({
        url: `/tasks`,
        method: "POST",
        body: task,
 }),
      invalidatesTags: ["Tasks"],
 }),
    updateTask: builder.mutation({
      query: ({ id, task }: UpdateTaskProps) => ({
        url: `/tasks/${id}`,
        method: "PUT",
        body: task,
 }),
      invalidatesTags: ["Tasks"],
 }),
    removeTask: builder.mutation({
      query: (id: string) => ({
        url: `/tasks/${id}`,
        method: "DELETE",
 }),
      invalidatesTags: ["Tasks"],
 }),
 }),
});

export const {
  useGetTasksQuery,
  useAddTaskMutation,
  useUpdateTaskMutation,
  useRemoveTaskMutation,
} = tasksApiSlice;

The code snippet above includes additional mutation endpoints to update the data source. We also provided the invalidateTags to each mutation endpoint to invalidate the cached data. Let’s update the code in TaskForm.tsx and use these endpoints to mutate our data store:

// src/components/Form/TaskForm.tsx

const TaskForm = () => {
  // ...previous code here

  const [addTask] = useAddTaskMutation();

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    // ...previous data generation logic
    
    // Updated code for adding a task
    addTask(data)
 .unwrap()
 .then(() => {
        setFields({ task_name: "", do_at: "" });
 });
 };

  // UI markup and logic
};

export default TaskForm

The code within the TaskForm component renders a form with input fields for adding a task. It also includes a handleSubmit function that interacts with the addTask endpoint after a button click. However, compared to simple usage of RTK Query mutation hooks, we chained unwrap. The unwrap method allows us to access the success and error payload immediately after a mutation.

After a successful request, we should have task items we can edit and delete. Let’s update our existing codebase to achieve these:

// src/modules/tasks/index.tsx

// Previous imports
import {
  useRemoveTaskMutation,
  useUpdateTaskMutation,
} from "../../redux/slice/taskSlice";

const Tasks = ({ tasks, tab }: { tasks: Task[]; tab: TodoTabType }) => {
  const [updateTask] = useUpdateTaskMutation();
  const [removeTask] = useRemoveTaskMutation();

  // Initial code setup

  // Handler to complete a task
  const completeTask = (id: string) => {
    const completedTask = tasks.find((task) => id === task.id);

    if (completedTask) {
      updateTask({
        id,
        task: { ...completedTask, completed: !completedTask.completed },
 });
 }
 };

  // Handler to remove a task
  const removeTaskHandler = (id: string) => {
    removeTask(id);
 };

  // Initial component JSX code
};

export default Tasks;

We updated the code with instances from both useUpdateTaskMutation and useRemoveTaskMutation hooks. The updateTask function enables us to edit a task with a given id. In this case, we utilized it to mark a task as completed or pending. Similarly, the removeTask function removes a task by id from our data source. Furthermore, let’s update the code in the EditFormModal component to edit a task’s content:

// src/component/Form/EditFormModal.tsx

// Initial imports
import { useUpdateTaskMutation } from "../../redux/slice/taskSlice";

const EditFormModal = ({ show, close, tasks, id }: EditFormModalProps) => {
  // Initial component state code
  const [updateTask] = useUpdateTaskMutation();

  // ...previous code here

  const saveChanges = (e: FormEvent<HTMLFormElement>) => {
    // ...previous code here

    const updatedTask = tasks.find((task) => task.id === id);

    if (updatedTask) {
      updateTask({
        id,
        task: { ...updatedTask, name: taskName, startTime: taskDate },
 }).unwrap().then(() => {
        close();
 });
 }
 };

  // Previous component JSX code
};

export default EditFormModal;

Clicking the edit button displays a modal with a pre-filled form. This form contains input fields showing the selected task name and date. Leveraging the useEffect hook allowed us to update the inputs’ value when the id changes and the modal opens. In addition, any updates made to the inputs and saved reflect on the selected task item. Also, Invoking the useUpdateTaskMutation hook gave us access to the updateTask function. This function enabled us to update the selected task’s content. Finally, after a successful update, we utilized the mutation’s unwrap method to close the modal. Below is a visual representation of the UI while performing CRUD operations:

Performing CRUD operations

Leveraging caching

RTK Query allows us to benefit from caching, improving performance by preventing unnecessary server requests. To visualize its caching capability, let’s utilize the useGetTaskQuery hook in the EditFormModal component:

// src/component/Form/EditFormModal.tsx

const EditFormModal = ({ show, close, tasks, id }: EditFormModalProps) => {
  // Initial component state code
  const { data, isLoading, isSuccess } = useGetTasksQuery("");

  console.log({ data });

  // The rest of the component's code
};

The updated code above shows the data retrieved from the useGetTasksQuery hook. We logged the data value to the console to gain clarity of the RTK Query caching capability. Below is a visual aid:

Using RTK cached data after initial data retrieval

The visual representation above shows a server request to retrieve data when the Task component mounts. But clicking the edit button mounts the EditFormModal component without a new request, logging the data in the console. This illustrates the RTK Query basic caching capability. To explore the full its features, we need the Redux store.

Seamlessly Integrating RTK Query with Redux Toolkit

So far, we’ve explored RTK Query's data fetching and caching feature. We also used its generated hooks to manage these processes when components mount and unmount. This section focuses on its integration with the Redux Toolkit. This approach enables leveraging Redux’s full capability for managing application state and data. We will achieve this with the following steps:

  • Configure the Redux store.
  • Connect the API slice to the store.
  • Provide the store to the React application.

Configuring the Redux Store

The Redux Toolkit package provides the configureStore API to help simplify store creation. Let’s create a store.ts file with a store basic setup:

// src/redux/store

import { configureStore } from "@reduxjs/toolkit";

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

Employing an empty reducer object and configuring middleware with getDefaultMiddleware allowed us to maintain a minimal setup. Nevertheless, this setup simplifies extending the Redux store's configuration.

Connecting the API Slice to the Redux Store

By default, the API slice auto-generates the slice reducer and a custom middleware. This API middleware manages subscription lifetimes by enabling caching, invalidation, polling, etc. Let’s connect the API slice to the store:

// src/redux/store.ts

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

In the code snippet above, we provided the slice reducer to the store as a value of its reducerPath. We also added the API middleware to leverage RTK Query's full potential. However, the result of our current setup is unavailable to the React app. Let’s fix that in the coming sub-section.

Providing the Store to the React App

Previously, we wrapped the application with the ApiProvider to utilize RTK Query features. However, to use the store configured with the API slice, we will replace the ApiProvider with the Provider component. Consider the following code:

// src/App.tsx

// Updated: Replaced ApiProvider with Provider and the Redux store

function App() {
  return (
 <Provider store={store}>
 {/* Component */}
 </Provider>
 );
}

In the code above, we successfully replaced the ApiProvider with the Provider component. By wrapping the application with the react-redux Provider, we integrated the store and unlocked RTK Query's full capabilities.

Final Thoughts

This article explored RTK Query's data fetching and caching features and its integration with Redux Toolkit. We also discussed how its unique API design eliminates the need for hand-written code and streamlines its integration.

Furthermore, RTK Query's integration with Redux Toolkit offers several use cases beyond its lone usage. Therefore, understanding its proper usage will effortlessly assist React developers to create a better user experience in modern web applications.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay