React tutorial working with redux toolkit with saga in typescript

Author
April 06, 2022

In this article i will explain you step by step to use redux toolkit with saga in react typescript application

Step 1: Create react typescript application with following command

npx create-react-app my-app --template typescript

Step 2: Install redux, redux toolkit, redux saga, react-redux, redux-actions

In your newly created application install redux toolkit, redux saga and react-redux with following command

npm install --save react-redux redux-saga redux-actions @reduxjs/toolkit @types/react-redux @types/redux-saga @types/redux-actions

Install redux logger this helps in to log all triggered actions in the developer console

npm install --save-dev redux-logger @types/redux-logger

Install axios this helps in sending http request

npm install --save axios @types/axios

Step 3: Create Store

Store act as central repository for all your state management

Go to your project folder and create /src/store/index.ts this is the main file for your store creation where we will be initializing and creating store

import { configureStore } from "@reduxjs/toolkit";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./rootReducer";
import { rootSaga } from "./rootSaga";

export default function configureMainStore() {
  const sagaMiddleware = createSagaMiddleware();
  const store = configureStore({
    reducer: rootReducer,
    middleware: [sagaMiddleware, logger]
    devTools: process.env.NODE_ENV !== "production",
  });
  sagaMiddleware.run(rootSaga);
  return { store };
}

Step 4: Create Root Reducer

Reducers are the pure functions that take the current state and action and return the new state and tell the store how to do

Create a main reducer file as /src/store/rootReducer.ts where we will combine all the sub reducers and export it from here. Copy paste following content inside your rootReducer

import { combineReducers } from "@reduxjs/toolkit";
import { userslice } from "./user";
import { appslice } from "./app";

const reducers = combineReducers({
  user: userslice.reducer,
  app: appslice.reducer,
});

Step 5: Create Auth Reducer

We will be creating auth reducer as sub reducer i’m creating an example of login and signup to show you all how we can work for authentication in redux saga

Create a file /src/store/userslice.ts and copy paste the following code

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

const initialState: UserType = {
  userDetails: null,
  token: null,
};

export const userslice = createSlice({
  name: "user",
  initialState,
  reducers: {
    setAuthenticationToken: (
      state = initialState,
      { payload }: PayloadAction<any>
    ) => {
      return {
        ...state,
        token: payload,
      };
    },
    loginAction: (state = initialState, { payload }: PayloadAction<any>) => {
      return {
        ...state,
        userDetails: payload,
      };
    },
  },
  extraReducers: {},
});

Step 7: Add types for Auth

Create a file /src/auth/store/types/user.ts and copy paste following code

export type UserType = {
  userDetails: any,
  token: any,
};

Step 7: Create Auth Action types

Actions are plain JavaScript object that must have a type attribute to indicate the type of action performed. It tells us what had happened. Types should be defined as string constants in your application as given below

Create a file /src/store/actions/user.actions.types.ts and copy paste following code inside that file

import { createAction } from "redux-actions";

export const LOGIN = "LOGIN";
export const login = createAction(LOGIN);

export const REGISTER = "REGISTER";
export const register = createAction(REGISTER);

Step 8: Create Saga middleware for Auth

Redux Saga is a middleware library used to allow a Redux store to interact with resources outside of itself asynchronously

Create file /src/stores/sagas/user.ts copy paste the following code

import { all, put, takeLatest } from "redux-saga/effects";
import { errorMessage, successMessage } from "../../utilities/notification";
import { userslice } from "../slices/user";

import { LOGIN, REGISTER } from "../actions/user.actions.types";
import axios from "axios";
import * as Effects from "redux-saga/effects";
const call: any = Effects.call;

const login = async (payload: { email: string, password: string }) => {
  const { data } = await axios.post(
    "https://reqres.in/api/login",
    { email: payload.email, password: payload.password },
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );
  return data;
};

const signup = async (payload: { email: string, password: string }) => {
  const { data } = await axios.post(
    "https://reqres.in/api/register",
    { ...payload },
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  return data;
};

function* registerHandler({ payload: { data, callback } }: any) {
  try {
    const response: { token: string } = yield call(signup, {
      ...payload,
    });
    if (callback) {
      callback({ success: true, data: response.token });
    }
  } catch (error) {
    if (callback) {
      callback({ success: false, data: null });
    }
  }
}

function* loginSaga({ payload: { data, callback } }: any) {
  try {
    const response: { token: string } = yield call(signup, {
      ...payload,
    });
    yield put(userslice.actions.setAuthenticationToken(response.token));
    if (callback) {
      callback({ success: true, data: response.token });
    }
  } catch (error) {
    if (callback) {
      callback({ success: false, data: null });
    }
  }
}

function* authSaga() {
  yield all([takeLatest(LOGIN, loginSaga)]);
  yield all([takeLatest(REGISTER, registerHandler)]);
}

export default authSaga;

Step 9: Create Root saga which combine all the saga and export

Create a file /src/store/sagas/index.ts and copy paste the following code

import { all } from "redux-saga/effects";
import user from "./user";

const sagas = function* sagas() {
  yield all([user()]);
};

export default sagas;

Step 10:Add Store provider to your index.ts file

Inside your src/index.tsx file copy paste the following code

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";
import configureStore from "./stores";

const { store } = configureStore();

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Step 11:Inside your login page use redux saga

import React, { useRef } from "react";
import { login } from "../store/actions/user.actions.types.ts";
import { connect } from "react-redux";

const Login: React.FC<{}> = (props: any) => {
  const emailRef = useRef();
  const passwordRef = useRef();

  const callback = (data: any) => {
    console.log("Inside callback after login");
  };

  const login = () => {
    let data: any = {
      values: {
        email: emailRef.current.value,
        password: passwordRef.current.value,
      },
      callback,
    };
    props.login(data);
  };
  return (
    <div>
      <div className="form-floating">
        <input
          type="email"
          className="form-control"
          name="email"
          id="floatingInput"
          placeholder="name@example.com"
          ref={emailRef}
        />
        <label htmlFor="floatingInput">Email address</label>
      </div>

      <div className="form-floating mt-3">
        <input
          type="password"
          className="form-control"
          name="password"
          id="floatingPassword"
          placeholder="Password"
          ref={passwordRef}
        />
        <label htmlFor="floatingPassword">Password</label>
      </div>

      <div className="checkbox mb-3 mt-3">
        <label>
          <input name="remember" type="checkbox" defaultValue="remember-me" />{" "}
          Remember me
        </label>
      </div>
      <button
        onClick={() => {
          login();
        }}
        className="w-100 btn btn-lg btn-warning"
      >
        Sign in
      </button>
    </div>
  );
};

const mapDispatchToProps = (dispatch: any) => ({
  login: (params: any) => dispatch(login(params)),
});

export default connect(null, mapDispatchToProps)(Login);