DEV Community

Md Rashedul Alam Anik
Md Rashedul Alam Anik

Posted on

7

How to Implement Redux-Saga in React with TypeScript

How Redux-Saga with React-Redux works

Let’s see the following diagram to understand better:

High-level architecture diagram of how redux-saga and react-redux works

Redux-Saga and React-Redux work together to manage asynchronous operations (like API calls) and side effects (like data manipulation) in React applications.

When a React component dispatches an action (e.g., “FETCH_DATA”) to the Redux store, it passes through Redux middleware, including Redux-Saga. Redux-Saga triggers the corresponding saga function if a saga is “watching” for that specific action type.

This saga function, written using generators, manages the asynchronous flow. It might make an API call using libraries like Axios or fetch. The saga can also perform other actions, like waiting for promises to resolve or dispatching new actions based on conditions.

Once the saga finishes its work (e.g., receiving data from the API), it might dispatch a new action to update the Redux store state. React components connected to the store using React-Redux will detect this state change and re-render themselves with the updated data, reflecting the results of the asynchronous operation in the UI.

Let’s jump into the implementation.

First, let’s create a new React project by running the following command:



npm create vite@latest react-redux-saga-example -- --template react-ts


Enter fullscreen mode Exit fullscreen mode

Installing Packages

After creating a React project, let’s configure the project structure. Let’s first install React-Redux, Redux Toolkit, and Redux Saga. We can install these packages by running the following command:



npm install react-redux @reduxjs/toolkit redux-saga


Enter fullscreen mode Exit fullscreen mode

Let’s also install other necessary packages which will be required in the future:



npm install axios react-router-dom


Enter fullscreen mode Exit fullscreen mode

Setup

After we install the necessary packages, let’s implement our project structure like the following:

Project Structure

Here, we can see some folders. Let’s discuss one by one:

  1. Constants: Contains some constant variables that can be used in multiple files and components.
  2. lib: Contains useful functions and libraries.
  3. pages: Contains different page components that will be rendered based on route URL.
  4. store: All the logic for react-redux and redux-saga goes into the store folder.
  5. types: Contains necessary types based on project domain and scope.

[Note:* This project can be found in this SlackBlitz repo. So be sure to check that out.]*

Understanding Redux Saga and React Redux

Now, let’s first create store/index.ts and paste the following code:



import createSagaMiddleware from '@redux-saga/core';
import { configureStore } from '@reduxjs/toolkit';
import rootReducers from './root-reducer';
import rootSaga from './root-saga';

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
  reducer: rootReducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(sagaMiddleware),
});

sagaMiddleware.run(rootSaga);

export default store;


Enter fullscreen mode Exit fullscreen mode

This file configures the store to work with Reducer and integrate with redux-saga middleware. Don’t worry about the rootReducers and rootSaga for now, we will touch them eventually.

Let’s now create root-reducer.ts inside the same store folder, and paste the following code:



import usersReducer from './slices/users.slice';
import { UsersStateType } from '../types/user.types';

export type StateType = {
  users: UsersStateType;
};

const rootReducers = {
  users: usersReducer,
};

export default rootReducers;


Enter fullscreen mode Exit fullscreen mode

In this code, we combine all the reducers to pass them into the store configuration.

Let’s now create the user reducer in the store/slices/users.slice.ts file and paste the following code:



import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { USERS, UsersStateType, UserType } from '../../types/user.types';

const usersInitialState: UsersStateType = {
  user: {
    data: null,
    isLoading: false,
    errors: '' as unknown,
  },
  list: {
    data: [],
    isLoading: false,
    errors: '' as unknown,
  },
};

export const usersSlice = createSlice({
  name: USERS,
  initialState: usersInitialState,
  reducers: {
    getUserAction: (
      state: UsersStateType,
      { payload: _ }: PayloadAction<string>
    ) => {
      state.user.isLoading = true;
      state.user.errors = '';
    },
    getUserSuccessAction: (
      state: UsersStateType,
      { payload: user }: PayloadAction<UserType>
    ) => {
      state.user.isLoading = false;
      state.user.data = user;
    },
    getUserErrorAction: (
      state: UsersStateType,
      { payload: error }: PayloadAction<unknown>
    ) => {
      state.user.isLoading = false;
      state.user.errors = error;
    },
    getUserListAction: (state: UsersStateType) => {
      state.list.isLoading = true;
      state.list.errors = '';
    },
    getUserListSuccessAction: (
      state: UsersStateType,
      { payload: list }: PayloadAction<UserType[]>
    ) => {
      state.list.isLoading = false;
      state.list.data = list;
    },
    getUserListErrorAction: (
      state: UsersStateType,
      { payload: error }: PayloadAction<unknown>
    ) => {
      state.list.isLoading = false;
      state.list.errors = error;
    },
  },
});

export default usersSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

Here, we are implementing usersSlice using the createSlice method from the redux-toolkit. We are defining all the possible actions and returning the user reducer created from the usersSlice. Then, we pass the userReducer to the root reducer object.

Let’s now create our user saga to get the resources from the API. Let’s create store/sagas/users.saga.ts and paste the following code:



import { PayloadAction } from '@reduxjs/toolkit';
import { AxiosResponse } from 'axios';
import { put, takeLatest } from 'redux-saga/effects';
import { usersSlice } from '../slices/users.slice';
import apiClient from '../../lib/apiClient';
import { ApiEndpoints } from '../../constants/api';
import {
  GET_USER_BY_ID,
  GET_USER_LIST,
  UserType,
} from '../../types/user.types';

function* getUserSaga({ payload: id }: PayloadAction<string>) {
  try {
    const response: AxiosResponse<UserType> = yield apiClient.get(
      `${ApiEndpoints.USERS}/${id}`
    );
    yield put(usersSlice.actions.getUserSuccessAction(response.data));
  } catch (error) {
    yield put(usersSlice.actions.getUserErrorAction(error as string));
  }
}

function* getUserListSaga() {
  try {
    const response: AxiosResponse<UserType[]> = yield apiClient.get(
      `${ApiEndpoints.USERS}`
    );
    yield put(usersSlice.actions.getUserListSuccessAction(response.data));
  } catch (error) {
    yield put(usersSlice.actions.getUserListErrorAction(error as string));
  }
}

export function* watchGetUser() {
  yield takeLatest(GET_USER_BY_ID, getUserSaga);
  yield takeLatest(GET_USER_LIST, getUserListSaga);
}


Enter fullscreen mode Exit fullscreen mode

Here, we fetch the API responses using the generator function and yield the returned objects to the redux-saga middleware.

Let’s see the API responses in action

First, let’s add the react-redux provider in our root component:



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

import './index.css';

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


Enter fullscreen mode Exit fullscreen mode

Now, let’s see the below component how we can show the response in our component:



import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { StateType } from '../store/root-reducer';
import { usersSlice } from '../store/slices/users.slice';
import { Link } from 'react-router-dom';
import { PageRoutes } from '../constants/page-routes';

function Users() {
  const { list } = useSelector((state: StateType) => state.users);

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(usersSlice.actions.getUserListAction());
  }, []);

  return (
    <div>
      {list.isLoading ? (
        <span>Loading...</span>
      ) : list.data && list.data.length > 0 ? (
        <div>
          {list.data.map((user) => (
            <div key={user.id}>
              <Link to={`${PageRoutes.USERS.HOME}/${user.id}`}>
                {user.name}
              </Link>
            </div>
          ))}
        </div>
      ) : (
        <span>No users found!</span>
      )}
    </div>
  );
}

export default Users;


Enter fullscreen mode Exit fullscreen mode

It will render the following display in our browser:

Response from the API

That’s all in this article! I hope you enjoy it.

Have a great day!

Neon image

Build better on Postgres with AI-Assisted Development Practices

Compare top AI coding tools like Cursor and Windsurf with Neon's database integration. Generate synthetic data and manage databases with natural language.

Read more →

Top comments (0)

PulumiUP 2025 image

PulumiUP 2025: Cloud Innovation Starts Here

Get inspired by experts at PulumiUP. Discover the latest in platform engineering, IaC, and DevOps. Keynote, demos, panel, and Q&A with Pulumi engineers.

Register Now

Pieces AI Productivity Summit

​Join top AI leaders, devs, & enthusiasts for expert talks, live demos, and panels on how AI is reshaping developer productivity at the Pieces AI Productivity Summit.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️