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:

  1. Basic JavaScript Knowledge

  2. React Framework

  3. Redux

  4. APIs and Data Fetching

  5. Environment Variables

  6. 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 the api.injectEndpoints() method to define and configure specific API endpoints within the usersApi.

  • Inside endpoints: (build) => ({ ... }), the code defines three API endpoints: register, getAuthUser, and onboardUser.

  • Each endpoint definition includes a build.mutation or build.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 existing api instance. This means you can now use these endpoints directly from usersApi.

  • 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:

    • useRegisterMutationfor theregister mutation endpoint, which can be used to send registration data to the server.

    • useGetAuthUserQueryfor thegetAuthUser query endpoint, which can be used to retrieve information about the authenticated user.

    • useOnboardUserMutationfor 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:

  1. api.reducer: Manages API data using RTK Query.

  2. auth: Handles authentication state via a Redux slice reducer.

  3. usersReducerCustom 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 the rootReducer with all combined reducers.

  • The middleware property configures store middleware. It initially uses getDefaultMiddleware() for Redux Toolkit's default middleware and then adds RTK Query's middleware (api.middleware).

  • AppDispatchrepresents the store's dispatch function.

  • useAppDispatchis a hook granting access to the dispatch function, enabling action dispatching in components.

  • RootStatetypifies the complete Redux store state.

  • useTypedSelectoris 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.🤗

18
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.