In this guide, I will show you how to set up Redux Toolkit and RTK Query with React and TypeScript the right way. Although it’s possible to bootstrap a React app with Redux Toolkit support using a single command, this may not be sufficient for more complex state management requirements. In this guide, I’ll go beyond the basics and cover the steps needed to get both Redux Toolkit and RTK Query up and running in your project.

Since the official create-react-app tool has been abandoned by the React team, we’ll be using Vite instead as the module bundler for the project. While adding RTK Query to Redux Toolkit isn’t strictly necessary, combining the two in a React app can unleash the full power of Redux Toolkit.

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

Technology Stack

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

Prerequisites

How to Read This Tutorial Guide

This tutorial only focuses on setting up Redux Toolkit and RTK Query with React. It assumes that you already have a good understanding of Redux and know how to manage state with it in a React application.

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

In the past, we could easily set up a new React project using the create-react-app tool. However, with the introduction of Vite, the React team abandoned the create-react-app project in favour of Vite, which is known for its impressive speed and performance. In the latest React documentation, Vite is now recommended as the module bundler for developers who want to use React without a framework.

The recommended way to Add Redux Toolkit to React

Let’s start with the recommended way to bootstrap a React project that supports Redux Toolkit out of the box. The process is straightforward, and it involves running a single command that generates the project. The official Redux+TS template for Vite serves as the basis for this command. This template includes a small example application that showcases several features of Redux Toolkit.

To create a new React app that utilizes Redux Toolkit, you can execute the following command. It’s quicker and easier than setting up the project from scratch, and it helps to avoid errors:


# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app

This command uses npx degit to clone a repository template for a React app with Redux Toolkit support.

  • reduxjs/redux-templates is the GitHub repository containing the templates for creating Redux apps.
  • packages/vite-template-redux is the specific template for a React app using Redux Toolkit and Vite as the module bundler.
  • my-app is the name of the new directory that will be created to hold the new app. You can choose any name you want.

After running this command, a new directory named my-app will be created in your current directory, which contains the cloned template files. You can then cd into this new directory and start working on your React app with Redux Toolkit and Vite already set up.

Add Redux Toolkit to an Old React Project

If you’re working on an existing React project and want to integrate Redux Toolkit, this method is for you. However, if you’re starting a new project with Redux, I suggest using the official way to generate a pre-configured React project with Redux Toolkit for a faster and more straightforward setup.

But if you’re interested in learning how to set up Redux Toolkit and RTK Query with React from scratch, you’re in the right place. The process is not difficult, and you’ll gain a better understanding of how Redux Toolkit works. I’ll be using PNPM as the package manager, but you can use NPM or Yarn if you prefer. The choice of package manager won’t affect the code we write.

Along the way, I’ll share some best practices for using Redux Toolkit with React.

Initialize a New React App

Before we proceed with installing the necessary dependencies, let’s start by creating a new React app with TypeScript support using Vite. Depending on your preferred package manager, choose one of the following commands to initialize your project:


# NPM
npm create vite@latest my-react-app -- --template react-ts
# Yarn
yarn create vite my-react-app --template react-ts
# PNPM
pnpm create vite my-react-app --template react-ts

After generating the project, navigate to the project directory and install the necessary dependencies using the command that corresponds to your package manager.

Depending on your internet speed, the installation process may take a few minutes. You can take a break and enjoy a cup of coffee while Vite takes care of the installation in the background.

Install Redux Toolkit and React-Redux

Let’s now install the necessary dependencies to use Redux Toolkit with our React project. We’ll need to install two packages: @reduxjs/toolkit and react-redux.

The @reduxjs/toolkit package offers various tools and abstractions that simplify the process of working with Redux store and reducers. On the other hand, react-redux provides us with useful APIs to connect our React components to the Redux store and dispatch actions to modify the store.

To install these packages, simply run the following command:


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

Since Redux Toolkit is already written in TypeScript, we don’t have to worry about installing its type definition files separately. Additionally, react-redux has a dependency on @types/react-redux, so the type definition file for the package will be automatically installed along with it. This makes it easy for us to work with types and ensure type safety in our code.

Create a Redux Store

With the dependencies installed, we can move on to setting up the Redux store. While it’s possible to keep the store directly in the ‘src‘ directory, creating a dedicated ‘redux‘ folder within ‘src‘ can help with organization, especially for larger projects. We’ll use this folder to keep all Redux-related files and folders.

To organize our Redux files, we’ll create a new folder named ‘redux’ inside the ‘src’ directory. Inside the ‘redux’ folder, create a store.ts file which will contain the configurations needed to set up Redux Toolkit and RTK Query.

To create a store in Redux Toolkit, we’ll use the configureStore API. This function provides a standard abstraction over the createStore function and includes some default configurations for a better development experience.

The configureStore function takes a single configuration object with several properties, including the reducer, devTools, middleware, enhancers, and preloadedState. For our purposes, we’ll focus on the three essential properties: reducer, devTools, and middleware.

src/redux/store.ts


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

export const store = configureStore({
  reducer: {},
  devTools: import.meta.env.NODE_ENV !== "production",
  middleware: (getDefaultMiddleware) => getDefaultMiddleware({}).concat([]),
});

Define the Root State and Dispatch Types

To ensure that the RootState and AppDispatch types stay up-to-date with changes to the store, we should export them directly from the store.ts file. This way, the types will be inferred from the store itself, which means they will be updated automatically as we add more state slices, API services, or modify middleware settings.

src/redux/store.ts


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

export const store = configureStore({
  reducer: {},
  devTools: import.meta.env.NODE_ENV !== "production",

  middleware: (getDefaultMiddleware) => getDefaultMiddleware({}).concat([]),
});

// 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

Now that we have created the Redux store, we need to make it accessible to all our components. We can achieve this by using a special component called <Provider> that is available in the react-redux package. This component accepts our store as a prop and makes it available to all the child components.

It’s important to note that it’s recommended to render the <Provider> component at a level that is closer to its consumers. In our case, since we want to make the store accessible to all components, we can render it in the main.tsx file.

To do this, open your main.tsx file and start by importing the store from the ./src/redux/store.ts file. Then, wrap the <Provider> component around the <App /> component and provide the store as a prop. This way, all the components that are descendants of the <App /> component can have access to the Redux store.

src/main.tsx


import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App.tsx";
import "./index.css";
import { store } from "./redux/store.ts";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Define the State Selector and Dispatch Typed Hooks

Although you can import the RootState and AppDispatch types defined in store.ts into each component that needs them, it’s recommended to create typed versions of the useDispatch and useSelector hooks for use throughout your entire application. Here are a few reasons why you might prefer to use typed hooks instead of importing types into each component:

  • For useSelector hook, it saves you the need to type (state: RootState) whenever you want to access the state.
  • For the useDispatch hook, you need to use the customized AppDispatch type that we defined in store.ts to dispatch thunks correctly, since the default Dispatch type doesn’t include thunk middleware. By creating a pre-typed useDispatch hook, you avoid the need to import AppDispatch repeatedly whenever you dispatch an action.

To prevent circular import dependency issues, it’s recommended to define the typed useDispatch and useSelector hooks in a separate file from the store.ts file. This way, you can easily import the hooks into any component that needs to use them.

Create a new hooks.ts file within the redux folder at ./src/redux, and add the code snippets below to define the hooks.

src/redux/hooks.ts


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

Create a Redux State Slice and Action Types

To keep your Redux state slices organized, it’s recommended to create a ‘features‘ directory within the redux folder, and then create a subdirectory with the name of the resource you want to manage the state for. For example, if you want to manage states for products, name the subdirectory ‘products‘. If you want to manage states for users, name the subdirectory ‘users‘.

Let’s say you want to manage states for users, create a ‘users’ directory within the src/redux/features/ folder. Inside the ‘users‘ directory, create an authSlice.ts file and add your slice code to it. Here’s an example to guide you.

src/redux/features/users/authSlice.ts


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

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;

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;
}

If you’ve worked with Redux before, you’re probably familiar with the need to update the state in an immutable manner, by making copies of the state and updating the copies.

However, Redux Toolkit makes it easier by allowing you to mutate the state directly. This is made possible by Immer, a library used under the hood by Redux Toolkit’s createSlice and createReducer APIs.

With Immer, we can write “mutating” update logic that eventually becomes immutable updates, without having to manually create copies of the state.

Add the Reducer of the Redux State Slice to the Store

To connect the authSlice reducer with the store, we must import the authReducer function exported from the authSlice.ts file into the store.ts file and add it to the reducers object. By doing this, we inform the store to use the authReducer function to manage all updates to the authState state slice.

src/redux/store.ts


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

export const store = configureStore({
  reducer: {
    // ? Add the authReducer to the reducer object
    authState: authReducer,
  },
});

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

Add RTK Query to an Old React Project

RTK Query is a powerful data fetching and caching tool designed to work seamlessly with Redux Toolkit. It utilizes Redux Toolkit’s APIs such as createSlice and createAsyncThunk for its implementation.

While using RTK Query with Redux Toolkit is not mandatory, it’s highly recommended if you need to manage both local state and API data. Although there are other API state management tools like React Query, integrating it with Redux Toolkit can be quite cumbersome.

That’s why the Redux team created RTK Query, which is similar to React Query but is more compatible and easier to use with Redux Toolkit. By combining the two, you can easily manage your entire application’s state in a streamlined manner.

Create an API Service

The best practice recommended by the Redux team is to keep the API request files related to RTK Query in a dedicated folder called ‘services’, located within the ‘redux’ directory of your project. Each folder within the src/redux/services directory should have the name of the resources that will be queried or mutated on the API server.

For instance, if you plan to fetch and mutate products, you should name the folder ‘products’. Similarly, if you are working with users, you should name the folder ‘users’. You can easily determine the name of the resource from the URL like /api/users/:id.

In our example, we will be working with a ‘products‘ endpoint, so the folder will be named ‘products‘. Inside this folder, we’ll create a file called productApi.ts, which will contain the necessary code responsible for generating the query and mutation hooks.

Here’s a sample CRUD operation with RTK Query, where we’ll be fetching all the products, getting a single product, updating a single product, and deleting a single product.

src/redux/services/products/productApi.ts


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

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,
} = productApi;

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;
};

The createApi function is used to create an API client. It takes a configuration object that accepts several parameters:

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

In the example code provided, a productApi object is created using the createApi function. This object has several properties, each of which is a hook generated by createApi.

The endpoints property is an object containing the API endpoint logic for different operations such as fetching all products, fetching a single product, creating a product, updating a product, and deleting a product. Each endpoint has a ‘query‘ property that defines the operation to be performed and a transformResponse property that transforms the response data.

Add the API Service to the Redux Store

RTK Query’s createApi function generates a slice reducer from the productApi object, which should be added to the root reducer in your Redux store. Additionally, RTK Query also generates a custom middleware that should be added to the middleware parameter in your Redux store configuration.

src/redux/store.ts


import { configureStore } from "@reduxjs/toolkit";
// ? import authReducer from authSlice
import authReducer from "./features/users/authSlice";
import { productApi } from "./services/products/productApi";

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

// Infer the `RootState` and `AppDispatch` types from the store itself
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

Throughout this mini crash course, we’ve explored two different approaches to setting up Redux Toolkit and RTK Query with React and TypeScript: the recommended method, and a from-scratch approach. I hope that this tutorial has been helpful and informative for you.

If you have any questions or feedback, feel free to leave a comment below, and I’ll do my best to respond promptly. Additionally, you can find the source code for the examples shown in this article on our GitHub repository at https://github.com/wpcodevo/setup-redux-toolkit.

You can also read: