React tutorial using redux saga in react typescript application

Author
April 06, 2022

In this article i will explain you step by step to use redux 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 saga, redux logger, axios in your newly created application

In your newly created application install redux , redux saga with following command

npm install --save redux react-redux redux-saga @types/react-redux @types/redux-saga

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 { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";

import rootReducer from "./rootReducer";
import { rootSaga } from "./rootSaga";

// Create the saga middleware
const sagaMiddleware = createSagaMiddleware();

// Mount it on the Store
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware, logger));

// Run the saga
sagaMiddleware.run(rootSaga);

export default 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 "redux";

import authReducer from "./auth/reducer";

const rootReducer = combineReducers({
  auth: authReducer,
});

export type AuthState = ReturnType<typeof rootReducer>;

export default rootReducer;

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/auth/reducer.ts and copy paste the following code

import {
  LOGIN_REQUEST,
  LOGIN_SUCCESS,
  LOGIN_FAILURE,
  SIGNUP_REQUEST,
  SIGNUP_SUCCESS,
  SIGNUP_FAILURE,
} from "./actionTypes";

import { AuthActions, AuthState } from "./types";

const initialState: AuthState = {
  pending: false,
  token: "",
  error: null,
};

const reducers = (state = initialState, action: AuthActions) => {
  switch (action.type) {
    case SIGNUP_REQUEST:
      return {
        ...state,
        pending: true,
      };
    case SIGNUP_FAILURE:
      return {
        ...state,
        pending: false,
        token: "",
        error: action.payload.error,
      };

    case SIGNUP_SUCCESS:
      return {
        ...state,
        pending: false,
        token: action.payload.token,
        error: null,
      };

    case LOGIN_REQUEST:
      return {
        ...state,
        pending: true,
      };
    case LOGIN_SUCCESS:
      return {
        ...state,
        pending: false,
        token: action.payload.token,
        error: null,
      };
    case LOGIN_FAILURE:
      return {
        ...state,
        pending: false,
        token: "",
        error: action.payload.error,
      };
    default:
      return {
        ...state,
      };
  }
};

export default reducers;

Step 6: 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/auth/actionTypes.ts and copy paste following code inside that file

export const LOGIN_REQUEST = "LOGIN_REQUEST";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAILURE = "LOGIN_FAILURE";

export const SIGNUP_REQUEST = "SIGNUP_REQUEST";
export const SIGNUP_SUCCESS = "SIGNUP_SUCCESS";
export const SIGNUP_FAILURE = "SIGNUP_FAILURE";

Step 7: Add types for Auth

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

import {
  LOGIN_REQUEST,
  LOGIN_SUCCESS,
  LOGIN_FAILURE,
  SIGNUP_REQUEST,
  SIGNUP_SUCCESS,
  SIGNUP_FAILURE,
} from "./actionTypes";

export interface IAuth {
  token: string;
}

export interface AuthState {
  pending: boolean;
  token: string;
  error: string | null;
}

export interface LoginPayload {
  values: { email: string, password: string };
  callback: any;
}

export interface LoginSuccessPayload {
  token: string;
}

export interface LoginFailurePayload {
  error: string;
}

export interface SignupSuccessPayload {
  token: string;
}

export interface SignupFailurePayload {
  error: string;
}

export interface LoginRequest {
  type: typeof LOGIN_REQUEST;
  payload: LoginPayload;
}

export type LoginSuccess = {
  type: typeof LOGIN_SUCCESS,
  payload: LoginSuccessPayload,
};

export type LoginFailure = {
  type: typeof LOGIN_FAILURE,
  payload: LoginFailurePayload,
};

export interface SignupPayload {
  values: { email: string, password: string };
  callback: any;
}

export interface SignupRequest {
  type: typeof SIGNUP_REQUEST;
  payload: SignupPayload;
}

export type SignupSuccess = {
  type: typeof SIGNUP_SUCCESS,
  payload: SignupSuccessPayload,
};

export type SignupFailure = {
  type: typeof SIGNUP_FAILURE,
  payload: SignupFailurePayload,
};

export type AuthActions =
  | LoginRequest
  | LoginSuccess
  | LoginFailure
  | SignupFailure
  | SignupSuccess
  | SignupRequest;

Step 8: Create Actions for Auth

Actions are generally passing of the payload inn form of object with action types which generally tells which action has to be done

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

import {
  LOGIN_REQUEST,
  LOGIN_FAILURE,
  LOGIN_SUCCESS,
  SIGNUP_REQUEST,
  SIGNUP_FAILURE,
  SIGNUP_SUCCESS,
} from "./actionTypes";
import {
  LoginPayload,
  SignupPayload,
  LoginRequest,
  LoginSuccess,
  LoginSuccessPayload,
  LoginFailure,
  LoginFailurePayload,
  SignupRequest,
  SignupSuccess,
  SignupSuccessPayload,
  SignupFailure,
  SignupFailurePayload,
} from "./types";

export const loginRequest = (payload: LoginPayload): LoginRequest => ({
  type: LOGIN_REQUEST,
  payload,
});

export const loginSuccess = (payload: LoginSuccessPayload): LoginSuccess => ({
  type: LOGIN_SUCCESS,
  payload,
});

export const loginFailure = (payload: LoginFailurePayload): LoginFailure => ({
  type: LOGIN_FAILURE,
  payload,
});

export const signupRequest = (payload: SignupPayload): SignupRequest => ({
  type: SIGNUP_REQUEST,
  payload,
});

export const signupSuccess = (
  payload: SignupSuccessPayload
): SignupSuccess => ({
  type: SIGNUP_SUCCESS,
  payload,
});

export const signupFailure = (
  payload: SignupFailurePayload
): SignupFailure => ({
  type: SIGNUP_FAILURE,
  payload,
});

Step 9: 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 a file /src/store/auth/saga.ts and copy paste the following code

import axios from "axios";
import { all, call, put, takeLatest } from "redux-saga/effects";

import {
  loginFailure,
  loginSuccess,
  signupSuccess,
  signupFailure,
} from "./actions";
import { LOGIN_REQUEST, SIGNUP_REQUEST } from "./actionTypes";
import { IAuth } from "./types";

const login = async (payload: { email: string; password: string }) => {
  const { data } = await axios.post<IAuth>(
    "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<IAuth>(
    "https://reqres.in/api/register",
    { ...payload },
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  return data;
};


function* loginSaga(action: any) {
  try {
    const response: { token: string } = yield call(login, {
      email: action.payload.values.email,
      password: action.payload.values.password,
    });

    yield put(
      loginSuccess({
        token: response.token,
      })
    );
    action.payload.callback(response.token);
  } catch (e: any) {
    yield put(
      loginFailure({
        error: e.message,
      })
    );
  }
}

function* signupSaga(action: any) {
  try {
    const response: { token: string } = yield call(signup, {
      email: action.payload.values.email,
      password: action.payload.values.password,
    });

    yield put(
      signupSuccess({
        token: response.token,
      })
    );
    action.payload.callback(response.token);
  } catch (e: any) {
    yield put(
      signupFailure({
        error: e.message,
      })
    );
  }
}

function* authSaga() {
  yield all([takeLatest(LOGIN_REQUEST, loginSaga)]);
  yield all([takeLatest(SIGNUP_REQUEST, signupSaga)]);
}

export default authSaga;

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

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

import { all, fork } from "redux-saga/effects";

import authSaga from "./auth/sagas";

export function* rootSaga() {
  yield all([fork(authSaga)]);
}

Step 11: Add Reselect

Using reselect has certain advantage it create memoization of the selector that are only re executed when arguments of the selector changes

Install reselect using following command

npm install --save reselect

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

import { createSelector } from "reselect";

import { AuthState } from "../rootReducer";

const getPending = (state: AuthState) => state.auth.pending;

const getToken = (state: AuthState) => state.auth.token;

const getError = (state: AuthState) => state.auth.error;

export const getAuthSelector = createSelector(getToken, (token) => token);

export const getPendingSelector = createSelector(
  getPending,
  (pending) => pending
);

export const getErrorSelector = createSelector(getError, (error) => error);

Step 12: 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 store from "./store";

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 13: Inside your login page use redux saga

import React, { useRef } from "react";
import { loginRequest } from "../store/auth/actions";
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(loginRequest(params)),
});

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