In this guide, I will show you how to set up Redux Toolkit and RTK Query with React and TypeScript the right way.

Related articles:

  1. React, Material UI and React Hook Form: Login and Signup Forms
  2. React, RTK Query, React Hook Form and Material UI – Image Upload
  3. React + Redux Toolkit: JWT Authentication and Authorization
  4. React.js + Redux Toolkit: Refresh Tokens Authentication
  5. React CRUD example with Redux Toolkit, RTK Query & REST API
How I Setup Redux Toolkit and RTK Query the right way

Adding RTK Query to Redux Toolkit is not mandatory but when you combine both of them in a React project it brings out the true power of Redux Toolkit.

Technology Stack

  • React
  • TypeScript
  • Redux Toolkit
  • React-redux
  • RTK Query

Prerequisites

How to Read this Tutorial Guide

This tutorial will focus on how to set up Redux Toolkit and RTK Query with React. I will assume you already have a good understanding of Redux and how to manage state with it in a React app.

For a more detailed explanation of what Redux is, how it works, and demos on how to use Redux Toolkit, check out the Redux overview tutorial.

The examples will be based on a typical Create React App project where all the code will be in the src folder. Also, I will provide some best practices to adopt when using Redux Toolkit with React.

The recommended way to Add Redux Toolkit to React

The recommended way to initialize a new app with React and Redux is by using the official Redux+JS template or Redux+TS template.

Creating a React app that uses Redux this way is a lot quicker and also prevents you from making mistakes.


# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript

Add Redux Toolkit to an Old React Project

This method is for those who want to add Redux Toolkit to their old React Projects.

If you also want to learn how to set up Redux Toolkit and RTK Query with React from scratch to understand the ins and outs of Redux Toolkit then you are at the right place.

When starting a new React project with Redux, I recommend you follow the recommended way to add Redux Toolkit to React since it’s quicker and easier to set up.

Am going to use Yarn as my package manager for this tutorial, you can use NPM if you are more comfortable with it. The package manager you use doesn’t affect the code we will write.

Initialize a New React App

Before we start fetching and installing the required dependencies, let’s first initialize a new React App if you don’t have one.

Run this command to create a new React app with TypeScript.


# NPM
npx create-react-app redux-app --template typescript
# Yarn
yarn create react-app redux-app --template typescript

The project initialization process will take a couple of minutes depending on your internet speed so sit back and grab some coffee while Create React App does its job in the background.

Install Redux Toolkit and React-Redux

Fetch and install Redux Toolkit and React-redux in the project.


# NPM
npm install @reduxjs/toolkit react-redux
# Yarn
yarn add @reduxjs/toolkit react-redux

Redux Toolkit is already written in Typescript so we don’t need to worry about installing its type definition files separately.

React redux has a dependency on @types/react-redux so the type definition file of the package will be automatically installed with it.

Create a Redux Store

Inside the src folder, create a redux folder and within this redux folder create a store.ts file.

Now your folder structure should look somewhat like this.

redux-app/
├── node_modules/
├── public/
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src/
│ ├── redux/
│ │ └── store.ts
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
├── .gitignore
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock

To create a store in Redux Toolkit, we have to use the configureStore API which is a standard abstraction over the createStore function but adds some good default configurations for a better development experience.

The configureStore function accepts a single configuration object with the following properties:

  • reducer
  • devTools
  • middleware
  • enhancers
  • preloadedState

We are going to focus on the three essential properties (reducer, devTools and middleware) to configure the store.


import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {}
})

We don’t need to provide the configureStore with any additional typings.

Define the Root State and Dispatch Types

We need to extract the RootState and AppDispatch from the store and export them directly from the store.ts file.

Inferring RootState and AppDispatch from the store itself means that they’ll correctly update as you add more state slices, API services or modify middleware settings.


import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {}
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getstate>
export type AppDispatch = typeof store.dispatch

Provide the Redux Store to the React App

Since the store has been created, we need to provide it to all our components from the top level of our application.

In the index.tsx file, import the store from ./redux/store and the <Provider> component from react-redux.

Wrap the Provider component around the app component and pass the store as a prop to the Provider.


import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// ? Import Provider from react-redux and store from ./redux/store
import { Provider } from 'react-redux';
import { store } from './redux/store';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    {/* ? Provide the store as prop */}
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Define the State Selector and Dispatch Typed Hooks

While it’s possible to import the RootState and AppDispatch types we defined inside the store.ts file into each component, it’s recommended to create typed versions of the useDispatch and useSelector hooks for usage in your entire application. Below are some of the reasons why you shouldn’t import the RootState or AppDispatch types into each component.

  • For useSelector hook, it saves you the need to type  (state: RootState) whenever you want to access the state.
  • For useDispatch hook, we need to use the customized AppDispatch type we defined in the store.ts file to dispatch thunks correctly since the default Dispatch type doesn’t know anything about thunks.
    The middleware types are also included in the AppDispatch type. Using a pre-typed useDispatch will prevent you from repeatedly importing the AppDispatch every time you need to dispatch an action.

Within the redux folder ./src/redux create a new hooks.ts file and add the code snippets below.


import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<appdispatch>();
export const useAppSelector: TypedUseSelectorHook<rootstate> = useSelector

Define the typed useDispatch and useSelector hooks in a separate file instead of the store.ts file. This will allow you to import them into any component that needs to make use of hooks and prevent you from falling into the circular import dependency issues.

Create a Redux State Slice and Action Types

Now it’s time to create our first redux state slice, inside the redux folder create a features directory to house all our slices.

Next, create a new file named src/redux/features/products/authSlice.ts .

copy and paste the code snippets below into the authSlice.ts file.


import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface IUser {
  _id: string;
  name: string;
  email: string;
  photo: string;
  role: string;
  provider?: string;
  active?: boolean;
  verified?: boolean;
  createdAt: Date;
  updatedAt: Date;
  __v: number;
  id: string;
}

interface AuthState {
  user?: IUser | null;
}

const initialState: AuthState = {
  user: null,
};

export const authSlice = createSlice({
  name: 'authSlice',
  initialState,
  reducers: {
    // ? Logout the user by returning the initial state
    logout: () => initialState,
    // Save the user's info
    userInfo: (state, action: PayloadAction<authstate>) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.user = action.payload.user;
    },
  },
});

export const { logout, userInfo } = authSlice.actions;
// ? Export the authSlice.reducer to be included in the store.
export default authSlice.reducer;


If you’ve used Redux in the past you know Redux requires us to write all state updates immutably by making copies of the state and updating the copies.

In Redux Toolkit we have the luxury to mutate any state directly.

Redux Toolkits createSlice and createReducer APIs use Immer under the hood to allow us to write “mutating” update logic that eventually becomes immutable updates.

Add the Reducer of the Redux State Slice to the Store

Next, we need to import the reducer function we exported from the authSlice.ts into the store and add it to the reducers object.

By defining the authUser field inside the reducer object, we tell the store to use the authReducer function to handle all updates to that state.


import { configureStore } from '@reduxjs/toolkit';
// ? import authReducer from authSlice
import authReducer from './features/authSlice'

export const store = configureStore({
  reducer: {
    // ? Add the authReducer to the reducer object
    authUser: authReducer
  },
// ? show the devTools only in development
devTools: process.env.NODE_ENV !== 'production',
});

export type RootState = ReturnType<typeof store.getstate>;
export type AppDispatch = typeof store.dispatch;


Add RTK Query to an Old React Project

RTK Query is an advanced data fetching and caching tool, designed to fetch and cache API data.

RTK Query is built on top of the Redux Toolkit core and uses Redux Toolkit’s APIs like createSlice and createAsyncThunk for its implementation.

It is not mandatory to use RTK Query with Redux Toolkit but it’s recommended to combine both Redux Toolkit and RTK Query in your project if you will be managing both local state and API data.

You can use other API state management tools like React Query but getting it up and running with Redux Toolkit is a pain in the ass.

That is why the Redux team decided to create RTK Query which looks similar to React Query but it’s more compatible and easier to use with Redux Toolkit.

Create an API Service

Create an api folder within the redux directory and create a file named src/redux/api/products/productAPI.ts .

Below is a CRUD operation with RTK Query where am fetching all the products, getting a single product, updating a single product and deleting a single product.


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

type IProduct = {
  _id: string;
  name: string;
  avgRating: number;
  numRating: number;
  price: number;
  description: string;
  countInStock: number;
  quantity?: number;
  imageCover: string;
  images: string[];
  category: string;
  createdAt: Date;
  updatedAt: Date;
  slug: string;
  id: string;
};

export const productApi = createApi({
  reducerPath: 'productApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/api/' }),
  tagTypes: ['Products'],
  endpoints: (builder) => ({
    // ? Query: Get All Products
    getProducts: builder.query<iproduct[], void>({
      query() {
        return 'products';
      },
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({
                type: 'Products' as const,
                id,
              })),
              { type: 'Products', id: 'LIST' },
            ]
          : [{ type: 'Products', id: 'LIST' }],
      // ? Transform the result to prevent nested data
      transformResponse: (response: { data: { products: IProduct[] } }) =>
        response.data.products,
    }),
    // ? Query: Get a single product
    getProduct: builder.query<iproduct, string>({
      query(id) {
        return `products/${id}`;
      },
      transformResponse: (
        response: { data: { product: IProduct } },
        args,
        meta
      ) => response.data.product,
      providesTags: (result, error, id) => [{ type: 'Products', id }],
    }),
    // ? Mutation: Create a product
    createProduct: builder.mutation<iproduct, formdata>({
      query(data) {
        return {
          url: 'products',
          method: 'POST',
          credentials: 'include',
          body: data,
        };
      },
      invalidatesTags: [{ type: 'Products', id: 'LIST' }],
      transformResponse: (response: { data: { product: IProduct } }) =>
        response.data.product,
    }),
    // ? Mutation: Update Product
    updateProduct: builder.mutation<
      IProduct,
      { id: string; formData: FormData }
    >({
      query({ id, formData }) {
        return {
          url: `products/${id}`,
          method: 'PATCH',
          credentials: 'include',
          body: formData,
        };
      },
      invalidatesTags: (result, error, { id }) =>
        result
          ? [
              { type: 'Products', id },
              { type: 'Products', id: 'LIST' },
            ]
          : [{ type: 'Products', id: 'LIST' }],
      transformResponse: (response: { data: { product: IProduct } }) =>
        response.data.product,
    }),
    // ? Mutation: Delete product
    deleteProduct: builder.mutation<null, string>({
      query(id) {
        return {
          url: `products/${id}`,
          method: 'DELETE',
          credentials: 'include',
        };
      },
      invalidatesTags: [{ type: 'Products', id: 'LIST' }],
    }),
  }),
});

export const {
  useCreateProductMutation,
  useUpdateProductMutation,
  useDeleteProductMutation,
  useGetProductsQuery,
  useGetProductQuery,
  usePrefetch,
} = productsApi;


createAPI accepts a single configuration object which has some of the following parameters:

  • reducerPath: used as the root state key when adding the reducer function to the store.
  • baseQuery: uses fetchBaseQuery which is a small wrapper around the fetch API
  • endpoints: a function that returns an object containing all the API endpoint logics.

Add the API Service to the Redux Store

RTK Query will generate a “slice reducer” from the productAPI that should be added to the Redux root reducer.

A custom middleware will also be generated and we need to add it to the middleware parameter.


import { configureStore } from '@reduxjs/toolkit';
// ? import authReducer from authSlice
import authReducer from './features/authSlice';
import { productsApi } from './api/products/productAPI';

export const store = configureStore({
  reducer: {
    // ? Add the authReducer to the reducer object
    authUser: authReducer,
    [productsApi.reducerPath]: productsApi.reducer,
  },
  devTools: process.env.NODE_ENV !== 'production',
   // Adding the api middleware enables caching, invalidation, polling,
   // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({}).concat([productsApi.middleware]),
});

export type RootState = ReturnType<typeof store.getstate>;
export type AppDispatch = typeof store.dispatch;


Adding the API middleware enables caching, polling, invalidation, and other essential features on RTK Query.

Conclusion

In this mini crash course, we looked at how to set up Redux Toolkit and RTK Query with React and TypeScript.

You can also read: