Data Fetching Made Effortless with RTK Query
This article is targeted at people who have already learned React and Redux, and need a guide to the RTK Query application. So, if you're still learning JavaScript, you might want to just skim through or follow my page for whenever articles of your interest are released.
Prerequisites
Before you read this article, it is best you have an understanding of:
Basic JavaScript Knowledge
React Framework
Redux
APIs and Data Fetching
Environment Variables
Foundational knowledge of Redux Toolkit (RTK) too.
Why RTK Query?
Redux Toolkit Query, also known as RTK Query, is a significant addition to the Redux ecosystem that is intended to help developers manage state and fetch data. Some of its benefits include:
Automating many of the repetitive and cumbersome tasks involved in API calls, from initiating the request to storing the results
It eliminates the need for manual setup since it comes with built-in caching, automatic re-fetching, and streamlined state updates.
It ensures optimal performance by minimizing unnecessary network requests and re-rendering.
In addition to being convenient, RTK Query prioritizes best practices. It also has a lot of interesting features, like the UI being updated even before the server responds, improving the experience for the users. Toolkit uses normalized catching to ensure that every unique data is stored once. It also uses automatic cache invalidation, where your application's state remains up-to-date as data is updated or deleted.
Setting Up RTK Query
With how convenient and interesting the usage of RTK Query is, it is no surprise that a lot of developers today are steering their wheels in that direction. To start using RTK Query in your codebase, you'll need to navigate into your base directory and ensure you have the Redux Toolkit installed.
npm install @reduxjs/toolkit react-redux
Structuring your CodeBase
A well-structured codebase is necessary while using RTK Query for maintainability, scalability, and easy collaboration with other developers. It also aids in debugging and testing. Developers new to the project can easily read, understand and collaborate on this codebase.
src/
|-- api/
| |-- user/
| | |-- userApiSlice.js
| | |-- userHooks.js
| |-- posts/
| | |-- postsApiSlice.js
| | |-- postsHooks.js
| |-- baseApi.js
|-- store/
| |-- store.js
|-- components/--
|-- .env
|-- baseUrl.js
|-- App.js
...
Storing The URL for Easy Access
There's a URL you're fetching data from. To store it for very easy access, we added the baseUrl.js
and the .env
file. In the .env
file, you define the environment variable of your root URL;
REACT_APP_BASE_URL=https://api.example.com
In the baseUrl.js
file, you can access the environment variable using process.env
;
export const baseUrl = process.env.REACT_APP_BASE_URL;
Using Vite, you should note that it becomes VITE_APP_BASE_URL= https://api.example.com
and .export const baseUrl = import.meta.env.VITE_APP_BASE_URL
respectively instead.
The Foundation; Your Base API
The first thing you're supposed to do after understanding the structure is set up the baseApi.js where the root URL of all the API endpoints is defined.
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
import { RootState } from '../store'
import { baseUrl } from '@/utils/baseUrl'
const baseQuery = fetchBaseQuery({
baseUrl: baseUrl,
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token
if (token) {
headers.set('authentication', `Bearer ${token}`)
}
return headers
},
})
const baseQueryWithRetry = retry(baseQuery, { maxRetries: 2 })
export const api = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithRetry,
endpoints: () => ({}),
})
This is a configuration setup for the requests we're going to be making. The createApi
is a function to create an API instance, the fetchBaseQuery
is used to make HTTP requests with a base URL. The retry
is used for handling request retries, and RootState
is a type representing the root state of the Redux store. We'd discuss more about this RootState
later on when we're setting up our store.
The prepareHeaders
function adds an authentication header to the HTTP request if a user token exists in the Redux store. This part is not necessary if you're not dealing with login and authentication.
Creating Specific API Slices
API slices allow you to define your API endpoints specifically using a clear and declarative syntax. You specify the following information for each endpoint:
The name of the endpoint.
The HTTP method (e.g., GET, POST, PUT, DELETE).
The URL or path for the API endpoint.
Other configuration options like query parameters, headers, and more.
You should note that once you define an API slice, RTK Query automatically generates Redux actions and reducers for that endpoint.
The 'api' folder contains our API slices. The two API slices we're looking at are the userApiSlice and the postApiSlice. We import the api
we exported from the baseApi.js
into each file. Imagine a blog post where there are multiple users and multiple posts from them. The userApiSlice targets any data fetching that has to do with the users, while the postApiSlice targets the data of the posts.
For the userApiSlice,
import { api } from '../baseApi.js';
export const usersApi = api.injectEndpoints({
endpoints: (build) => ({
register: build.mutation<RegisterPropsT, RegisterPropsT>({
query: ({ data }) => {
return {
url: `/users/registering`,
method: 'POST',
body: data,
}
},
}),
getAuthUser: build.query({
query: () => ({
url: `/users/authenticate`,
method: 'GET',
}),
}),
onboardUser: build.mutation({
query: ({ interestedProductCategories, dietPlan }) => ({
url: `/users/onboarding`,
method: 'PUT',
body: {
interestedProductCategories,
dietPlan,
},
}),
}),
}),
})
export const {
useRegisterMutation,
useGetAuthUserQuery,
useOnboardUserMutation,
} = usersApi
The
'api'
object from the'../baseApi.js'
is first imported, Then we make use of theapi.injectEndpoints()
method to define and configure specific API endpoints within theusersApi
.Inside
endpoints: (build) => ({ ... })
, the code defines three API endpoints:register
,getAuthUser
, andonboardUser
.Each endpoint definition includes a
build.mutation
orbuild.query
call, depending on whether it is for making a POST/PUT request (mutation) or a GET request (query).For example, the
register
mutation endpoint is defined with a URL for user registration, a POST request method, and the expected request body (data
).The defined endpoints are injected into the
usersApi
, which extends the existingapi
instance. This means you can now use these endpoints directly fromusersApi
.The code exports hook functions for each of the injected endpoints. They are used to interact with the API endpoints in your React components.
In this code, three hook functions are exported:
useRegisterMutation
for theregister
mutation endpoint, which can be used to send registration data to the server.useGetAuthUserQuery
for thegetAuthUser
query endpoint, which can be used to retrieve information about the authenticated user.useOnboardUserMutation
for theonboardUser
mutation endpoint, which can be used to onboard a user with specific product categories and diet plans.
Setting Up Your Store
When you make API requests, the data retrieved is stored in the store. When this data becomes stale or outdated, RTK Query can automatically re-fetch it from the API. It makes it so easy to retrieve up-to-date data without having to make multiple API calls.
RTK Query also generates Redux reducers and slices for each API endpoint you define. These reducers are responsible for updating the store with data received from the API. Each endpoint's slice in the store includes the loading state, error state, and data state, making it easy to track the status of API requests.
import { configureStore, combineReducers, getDefaultMiddleware } from '@reduxjs/toolkit';
import { api } from './services/api';
import auth from '../features/auth/authSlice';
import usersReducer from '../features/users/usersSlice';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
const rootReducer = combineReducers({
[api.reducerPath]: api.reducer,
auth,
user: usersReducer,
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch;
export type RootState = ReturnType<typeof store.getState>;
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
The combineReducers
function merges multiple reducers into a single root reducer, specifying how the app's state responds to actions. In this code, we combine three reducers:
api.reducer
: Manages API data using RTK Query.auth
: Handles authentication state via a Redux slice reducer.usersReducer
Custom Redux slice reducer for user data management.
The functions of the other part of the code:
The
reducer
property in the store setup points to therootReducer
with all combined reducers.The
middleware
property configures store middleware. It initially usesgetDefaultMiddleware()
for Redux Toolkit's default middleware and then adds RTK Query's middleware (api.middleware
).AppDispatch
represents the store's dispatch function.useAppDispatch
is a hook granting access to the dispatch function, enabling action dispatching in components.RootState
typifies the complete Redux store state.useTypedSelector
is a hook for secure data selection from the Redux store.
This code establishes a Redux store, configures it with middleware for efficient API data handling, combines multiple reducers, and exports essential store-related objects and hooks for use in React applications.
Using the Structured RTK Query in Components
import React from 'react';
import { useGetUserQuery } from '../api/user/userHooks';
function UserComponent({ userId }) {
const { data, error, isLoading } = useGetUserQuery(userId);
if (isLoading) return 'Loading...';
if (error) return `Error: ${error.message}`;
return <div>{data.name}</div>;
}
The above is a random example of fetching a user's data. To access every API slice in a component, you first have to import every hook you want to use. For example, to register a user following the same codebase, you start by importing the mutation of the register hook; the useRegisterMutation
hook.
import { useRegisterMutation } from './path-to-usersApi'; // Adjust the import path as needed
Inside your component, invoke the useRegisterMutation
hook to get access to the mutation function
const [registerUser, { isLoading, isError, isSuccess, data, error }] = useRegisterMutation();
Then, you use the registerUser
function to make the registration request when a user submits a registration form:
const handleRegistration = async (userData) => {
try {
const response = await registerUser({ data: userData });
// Handle successful registration, if needed
} catch (err) {
// Handle registration error
}
};
Wrapping Up
From this article:
You should have a good comprehension of RTK Query's Architecture with the contextual introduction and detailed code snippets.
You should recognize the importance of how RTK Query allows developers to separate data fetching logic from UI components, promoting reusability and modularity.
You should have an understanding of how endpoints are defined using createSlice and createAsyncThunk and how RTK Query manages concurrent requests, retries, and request cancellations.
You should have understood the initial steps of setting up RTK Query in a project, starting with installing the required packages.
You should know how to use API slices for endpoint specifications.
You should have learned how to configure the Redux store with RTK Query.
You should know how to utilize hooks in components for data fetching, enabling you to utilize this article in real-life concepts.
A well-organized RTK Query codebase simplifies development, making it easier to add features, fix bugs, and collaborate with others. By breaking things down, we ensure that our Redux logic remains clean, focused, and easy to understand, even as our application grows. It is no surprise that this technology is used by lots of developers.
If there's any other aspect of RTK Query you want me to write about, let me know in the comments.🤗
Subscribe to my newsletter
Read articles from Abolade Ilerioluwakiiye directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Abolade Ilerioluwakiiye
Abolade Ilerioluwakiiye
I'm Ilerioluwakiiye, a frontend developer, technical writer and other things not related to Hashnode. I've decided to grow my hobby of connecting with people with words. I hope I convey my view of things, as much as I hope you'll learn one or two from me.