Redux Toolkit-A Modern Redux: The Complete Guide(Part-2)

Redux Toolkit-A Modern Redux: The Complete Guide(Part-2)

This article covers Thunk middleware in detail and Extra Reducers and we will continue with the same application. You can read out Redux Toolkit part1

Requirement

When the user login into the application, the data should be saved inside the redux store.

Approach: The login functionality code will be asynchronous so will handle inside redux-thunk.

Redux-Thunk: Middleware that handles the asynchronous task.

When you install the toolkit library, thunk gets already installed, if you want to check then open the nodes modules and search for the redux-thunk

Screenshot 2022-10-07 at 11.10.33 AM.png

Redux Toolkit provides the createAsyncThunk method to use thunk middleware.

Currently, in our application the logic of login is written in the login.js file, now we will move the logic inside createAsyncThunk, and for that, we will create a new Slice AuthSlice.js, now you already know the process of how to create a slice and how to configure it with store, I will not waste your time in that

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

const initialState = {
    userInfo: {},
    userError: {},
    loading:true
};

const AuthSlice = createSlice({
   name: "auth",
   initialState,
   extraReducers:{}
});

I hope you already noticed that I have written extraReducers instead of reducers, you will get it in a few minutes, let's write our thunk logic for login. we need to import and call the createAsyncThunk method and that will be provided by Redux Toolkit

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

const userAuth  = createAsyncThunk('login', async(value) => {

});

CreateAsyncThunk

The first parameter is the unique string(you can pass anything) and the second is an async function that can receive two-parameter(the first one is value and the second is a thunk object, will discuss it later)

moving our login functionality inside an async function

export const userAuth = createAsyncThunk("user/login", async (value) => {
   const { email, password } = value;
   const response = await signInWithEmailAndPassword(auth, email, password);
   let userInfo = {};
   userInfo = {
      displayName: response.user.displayName,
      email: response.user.email,
      uid: response.user.uid,
   };
   return userInfo;
});

We have to return only those value that we want to save inside Redux and do not handle errors here we will handle everything in extra reducers

##ExtraReducers

This will handle the async task response, the async function we are passing inside the createAsyncThunk will return a promise, and the promise will be handled by extraReducers. let's check the code you will understand it

const AuthSlice = createSlice({
   name: "auth",
   initialState,
   extraReducers: (builder) => {
      builder.addCase(userAuth.pending, (state) => {
         state.loading = true;
      });
      builder.addCase(userAuth.fulfilled, (state, action) => {
         state.loading = false;
         state.userInfo = action.payload;
         state.error = "";
         state.isAuthenticated = true;
      });
      builder.addCase(userAuth.rejected, (state, action) => {
         state.loading = false;
         state.userInfo = {};
         state.error = action.error;
      });
   },
});

A promise can have only three states (pending, fulfilled, and rejected), so with the builder.addCase we handled all of the conditions. userAuth is the name of our thunk.

now let's extract our reducer from authSlice and check our final code

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase";

const initialState = {
   userInfo: {},
   userError: {},
   loading: true,
   isAuthenticated: false,
};

export const userAuth = createAsyncThunk("user/login", async (value) => {
   const { email, password } = value;
   const response = await signInWithEmailAndPassword(auth, email, password);
   let userInfo = {};
   userInfo = {
      displayName: response.user.displayName,
      email: response.user.email,
      uid: response.user.uid,
   };
   return userInfo;
});

const AuthSlice = createSlice({
   name: "auth",
   initialState,
   extraReducers: (builder) => {
      builder.addCase(userAuth.pending, (state) => {
         state.loading = true;
      });
      builder.addCase(userAuth.fulfilled, (state, action) => {
         state.loading = false;
         state.userInfo = action.payload;
         state.error = "";
         state.isAuthenticated = true;
      });
      builder.addCase(userAuth.rejected, (state, action) => {
         state.loading = false;
         state.userInfo = {};
         state.error = action.error;
      });
   },
});

export const AuthReducer = AuthSlice.reducer;

Now we have to dispatch our thunk action on the login page, open login.js, and write code for the handleSubmit method

const handleSubmit = (event) => {
      event.preventDefault();
      dispatch(userAuth(userInfo));
   };

and make sure you have added the authReducer inside the store.js

import { configureStore } from "@reduxjs/toolkit";
import { AuthReducer } from "./AuthSlice";
import { PostReducer } from "./PostSlice";

export const store = configureStore({
   reducer: {
      post: PostReducer,
      auth: AuthReducer,
   },
});

After doing all this user will be able to login into the application.

Second Scenario

Now let's check a condition where I have multiple thunks, we will move our google sign-in functionality into thunk. That will create a new thunk and will move our logic inside that

export const googleSignIn = createAsyncThunk("user/googleSignIn", async () => {
   const response = await signInWithPopup(auth, provider);
   await updateProfile(auth.currentUser, {
      displayName: response.user.displayName,
      phoneNumber: response.user.phoneNumber,
   });
   const userInfo = {
      displayName: response.user.displayName,
      email: response.user.email,
      phoneNumber: response.user.phoneNumber,
      uid: response.user.uid
   };
   await setDoc(doc(db, "Users", response.user.uid), userInfo);
   return userInfo;
});

Now let's write extra reducers for it

extraReducers: (builder) => {
      builder.addCase(userAuth.pending, (state) => {
         state.loading = true;
      });
      builder.addCase(userAuth.fulfilled, (state, action) => {
         console.log("action.payload", action.payload);
         state.loading = false;
         state.userInfo = action.payload;
         state.error = "";
         state.isAuthenticated = true;
      });
      builder.addCase(userAuth.rejected, (state, action) => {
         state.loading = false;
         state.userInfo = {};
         state.error = action.error;
      });
    builder.addCase(googleSignIn.pending, (state) => {
         state.loading = true;
      });
      builder.addCase(googleSignIn.fulfilled, (state, action) => {
         console.log("action.payload", action.payload);
         state.loading = false;
         state.userInfo = action.payload;
         state.error = "";
         state.isAuthenticated = true;
      });
      builder.addCase(googleSignIn.rejected, (state, action) => {
         state.loading = false;
         state.userInfo = {};
         state.error = action.error;
      });
   },

Did you notice that all userAuth and googleSignIn thunk is returning the same values, but we are writing 6 cases for that?

Note: If the cases are the same for two thunks then we can reduce it but if cases are different then we have to write as many cases as required

As we have the same cases for userAuth and GoogleSignIn, so we will reduce it to three, Instead of using the builder.addCase() we will call the builder.addMatcher().

extraReducers: (builder) => {
      builder.addMatcher(
         isAnyOf(userAuth.pending, googleSignIn.pending),
         (state) => {
            state.loading = true;
         }
      );
      builder.addMatcher(
         isAnyOf(userAuth.fulfilled, googleSignIn.fulfilled),
         (state, action) => {
            state.loading = false;
            state.userInfo = action.payload;
            state.error = "";
            state.isAuthenticated = true;
         }
      );
      builder.addMatcher(
         isAnyOf(userAuth.rejected, googleSignIn.rejected),
         (state, action) => {
            state.loading = false;
            state.userInfo = {};
            state.error = action.error;
         }
      );

IsAnyOf: This function is provided by Redux Toolkit which makes sure either of the condition should match. you can pass any number of cases inside isAnyOf, but make sure the cases should be the same.

now in login.js we will dispatch googleSignIn

 const handleGoogleButton = () => {
      dispatch(googleSignIn());
   };

Our Second scenario has been completed and you will be able to sign up with google.

Now we have covered most of the things, let's note some key points:

  1. In createAsyncMethod, the second parameter which is a function can accept two parameters first one is a value (which we send when we dispatch an action) and the second parameter is a think API

thunkApi:- gives us access to store state, dispatch etc(etc:- you can check in docs if you want more info mostly these two are helpful)

export const userAuth = createAsyncThunk("user/login", async (value,{getState, dispatch}) => {
   const state = getState(); // store state
   dispatch() // you can dispatch other actions from here
});

We have covered most of the things now, If you liked the content please follow me on LinkedIn and subscribe to my youtube channel. If you have any questions then please drop a comment.

Did you find this article valuable?

Support Aman Singh Tomar by becoming a sponsor. Any amount is appreciated!