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.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay β an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
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 theRTK Query
core functionality. This API accepts a collection of endpoint definitions for communicating with a specific server. ThecreateApi()
method also includes configuration for data retrieval and transformation. -
fetchBaseQuery()
: This API simplifies requests by wrapping thefetch API
. -
<ApiProvider />
: It can be used as aProvider
in the absence of the Reduxstore
. -
setupListeners()
: A utility that supports therefetchOnMount
andrefetchOnConnect
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 Reacthooks
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 theuseQuery
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:
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:
// 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:
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:
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:
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 thestore
. - 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.
Top comments (0)