How To Implement Redux Toolkit With React Js

Redux is a very popular state container for javascript and react apps. We can use it in react-native also. This is because we can handle app states globally anywhere throughout the app. Previously we are using redux-thunk or redux-saga for managing the redux state, but I recommend using redux-toolkit because it makes things easier for us. So in this article, I will explain how we can integrate the redux-toolkit in react js.

You will get official documentation for redux-toolkit from here Redux-Toolkit Link and basic redux from here Redux Link.

1. First, you need to create one react app using the create-react-app command as given below.

npx create-react-app reduxtoolkitdemo

2. Now you need to install dependencies like those given below.

yarn add @reduxjs/toolkit react-redux

3. So the redux toolkit includes many things like configureStore(), createReducer(), createAction(), createSlice(), createAsyncThunk(); I will explain them one by one.

4. First, we have to make a folder structure for managing the project. So we need to create one folder named ‘redux.’In that folder, we have to create one more folder called ‘ store,’ which contains a store.js file.

5. In the store.js file, we have to configure our redux app store using the configureStore() method. configureStore() method mostly all features of createStore(). It provides more features than createStore for developing apps in a better way.

It can redux-thunk middleware by default, and we can see it acting as a Redux dev tool in the browser. As of now, you can write the below code in your store.js file for beginning the app. We will modify this screen as per our requirements.

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
   reducer: {
      
   },
})

6. Now, if we want to access all the data from a store anywhere in the app, we have to configure it with the Provider component from ‘react-redux, which wraps our whole app. You can do this as given below.

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 './redux/store/store';
import { responsiveFontSizes, ThemeProvider } from '@mui/material';
import theme from './constants/theme';

const root = ReactDOM.createRoot(document.getElementById('root'));


root.render(
 <React.StrictMode>
   <Provider store={store}>
     <ThemeProvider theme={theme}>
       <App />
     </ThemeProvider>
   </Provider>
 </React.StrictMode>
);

reportWebVitals();

7. Now we will go for createSlice(). It takes a reducer function object, a slice name and an initial state value and automatically generates a slice reducer using the appropriate action builder and action type.

8. Now create one file named counterSlice.js. First, we have to import the createSlice method from the toolkit as given below.

import { createSlice } from '@reduxjs/toolkit'

9. It requires a unique name for identifying the slice for the store, initial value and one or more reducer functions to define how the state can be updated, and we can use it in required screens. After creating the slice, we export the auto-generated redux actions and reducer for the currentSlice. You can write code by the given below.

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
   value: 0,
}

export const counterSlice = createSlice({
   name: 'counter',
   initialState,
   reducers: {
       increment: (state) => {
           state.value += 1
       },
       decrement: (state) => {
           state.value -= 1
       },
       incrementByAmount: (state, action) => {
           state.value += action.payload
       },
   },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

const counterReducer = counterSlice.reducer

export default counterReducer

10. Now we have to add a reducer function of the current slice to the store. So after adding it, we can use the state anywhere in the app. So, for example, you can write code like the one below.

import { configureStore } from '@reduxjs/toolkit'

import counterReducer from '../slices/counterSlice'

export const store = configureStore({
   reducer: {
       counter: counterReducer,
   },
})

11. Now we will use redux state and actions in our app screens or components. We are using a functional part, so we have to use redux hooks to get data from the redux state using useSelector and dispatch the redux actions using useDispatch.

So first, we must create one component or screen file called ‘Counter.js’ where we will perform our activities. Then, you can write the code given below.

 import React, {  } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment, } from '../redux/slices/counterSlice'

export function Counter() {
   const count = useSelector((state) => state?.counter?.value)
   const dispatch = useDispatch()

   return (
       <div style={{ padding: '20px' }}>
           <div>
               <button
                   aria-label="Increment value"
                   onClick={() => {dispatch(increment())}}
               >
                   Increment
               </button>
               <span>{count}</span>
               <button
                   aria-label="Decrement value"
                   onClick={() => dispatch(decrement())}
               >
                   Decrement
             </button>
           </div>
       </div>
   )
}

12. So here, as per the above code, when we dispatch increment action, it will automatically create an action and type like the given below. So after that redux reducer state will increment the count as we wrote previously. And when the state updates, our Counter.js component updates the value, and we can show it on screens.

13. Now, if we want to define action and reducer in our way, we can also do it using createAction and createReducer methods of the toolkit. So I will explain it step by step.

Hire Our Expert React Js Developers

14. So we have to import createAction  from the toolkit and create and export actions with unique types like those given below. Here I created one action called multiplication with a special type counter/multiply using createAction method.

export const multiplication = createAction("counter/multiply");
export const incrementAmount = createAction("counter/incrementAmount");

15. Now we will create our reducer functions and export them. Previously we used switch cases for every action type and performed operations based on it. Now we will use the createReducer method of the toolkit and create the reducer. We can make cases in reducer using two ways, First is ‘builder callback’ notation, and the other is ‘map object’ notation.

16. So first, we will create a reducer using the ‘builder callback’ notation. So, in this case, it accepts two arguments: the initial value for the state and the callback function with one builder argument. Builder obj will provide a feature to add cases using addCase for defining actions the reducer handles. You can do it as given below.

import { createAction, createReducer } from '@reduxjs/toolkit'

const initialStateForExample = {
   value: 1
}

export const multiplication = createAction("counter/multiply");
export const incrementAmount = createAction("counter/incrementAmount");

//reducer
//way1 using builder callback notation
export const exampleSlice = createReducer(initialStateForExample, (builder) => {
   builder.addCase(multiplication, (state, action) => {
       state.value = state.value * 2
   });
   builder.addCase(incrementAmount, (state, action) => {
       state.value = state.value + action.payload;
   });
})

17. builder.addCase() has two arguments we need to pass, first is action type, for which case we need to handle reducer state, and the other is a reducer, which contains state and action. So the form has redux state value, and the act includes the type and coming payload.

18. We have to add this exampleSlice reducer to our store given below. I created it with another name for better understanding.

import { configureStore } from '@reduxjs/toolkit'
import counterReducer, { exampleSlice } from '../slices/counterSlice'

export const store = configureStore({
   reducer: {
       counter: counterReducer,
       example: exampleSlice,
   },
})

19. Now we will use this in our component. We will do the same kind of dispatch, and the reducer will update the value and can see the updated value in the same part. You can do it like the one given below.

import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment, incrementAmount, multiplication } from '../redux/slices/counterSlice'
import { fetchAllPosts } from '../redux/slices/postSlice'

export function Counter() {
   const count = useSelector((state) => state?.counter?.value)
   const example = useSelector((state) => state?.example)
   const dispatch = useDispatch()

   useEffect(() => {
       dispatch(fetchAllPosts())
   }, [])

   return (
       <div style={{ padding: '20px' }}>
           <div>
               <button
                   aria-label="Increment value"
                   onClick={() => {
                       dispatch(increment())
                   }}
               >
                   Increment
               </button>
               <span>{count}</span>
               <button
                   aria-label="Decrement value"
                   onClick={() => dispatch(decrement())}
               >
                   Decrement
               </button>

               <h4>Multiply example</h4>
               <span>{example?.value}</span>

               <button
                   aria-label="Multiply value"
                   onClick={() => {
                       dispatch(multiplication())
                   }}
               >
                   Multiply
               </button>
               <button
                   aria-label="Amount value"
                   onClick={() => {
                       dispatch(incrementAmount(5))
                   }}
               >
                   Add 5 Amount
               </button>
           </div>
       </div>
   )
}

20. Now we will create cases using map object notation. It accepts two arguments for basic usage. First is the initial state value, and second is the actionMap object, which handles the reducer for each required case. I will use the same action for incrementAmount and multiplication action types. You can do it as given below.

// way2 using map object notation
export const exampleSlice = createReducer(initialStateForExample, {
   [multiplication]: (state, action) => {
       state.value = state.value * 2
   },
   [incrementAmount]: (state, action) => {
       state.value = state.value + action.payload;
   }
})

21. You can use this in the same way as in steps 18 and 19.

22. When we want to implement an asynchronous function and need to manage response based on that, at that time, we have to go for the createAsyncThunk() method approach. It is a callback function that takes a string representing a Redux action type and should return a promise.

It provides a thunk action creator to execute the promise callback and dispatch the lifecycle actions based on the returned contract. It generates promise lifetime action types depending on the action type prefix that you pass in. It does not create any reducer functions.

Using this, we can write our logic and reducer, which are appropriate for our app. createAsyncThunk accepts 3 params: a string action type value, a payloadCreator callback, and an options object. We can do it in a simple way like the one given below.

import { createAsyncThunk, } from "@reduxjs/toolkit";
import { signInWithEmailAndPassword } from "firebase/auth";
import { FirebaseAuth } from "../../utils/firebase";
import { showCustomToast, toastType } from "../../utils/helpers";

export const loginWithPassword = createAsyncThunk('app/login', async (payload, { }) => {
       const { email, password } = payload
       const response = await signInWithEmailAndPassword(FirebaseAuth, email, password);
       console.log("response :", response);
       return {
           response
       }
})

23. If we want to use this promise value and response in the component, we must add this thunk action to the reducer. So the createSlice method has one prop called extraReducers to add our case for loginWithPassword type. We can use this case using the builder callback notation method because it’s a better approach, as given below.

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

export const loginWithPassword = createAsyncThunk('app/login', async (payload, { }) => {
       const { email, password } = payload
       const response = await signInWithEmailAndPassword(FirebaseAuth, email, password);
       console.log("response :", response);
       return {
           response
       }
})

export const loginSlice = createSlice({
   name: "login",
   initialState: {
       loading: false,
       success: false,
       error: null,
   },
   extraReducers: (builder) => {
       builder.addCase(loginWithPassword.fulfilled, (state, action) => {
           console.log("action :", action);
           state.loading = false;
           if (action?.payload?.success) {
               state.success = true;
               state.error = null;
           }
           else {
               state.success = false;
               state.error = action?.payload?.error;
           }
       })
   }
})

24. The payloadCreator function can be called with 2 types of arguments. The first is the payload, and the second contains thunkAPI.thunkAPI objects have parameters of Redux thunk functions like dispatch and getState, but it also includes some additional parameters like rejectWithValue, fulfillWithValue, extra, etc.

So as of now, we will use rejectWithValue, fulfillWithValue for our requirements. For more detail, you can check here.

25. For Asynchronous API calls if we want to handle success and failure state we can use rejectWithValue, fulfillWithValue  for asynchronous calls like given below:

 import { createAsyncThunk, createSlice, } from "@reduxjs/toolkit";
import { signInWithEmailAndPassword } from "firebase/auth";
import { FirebaseAuth } from "../../utils/firebase";
import { showCustomToast, toastType } from "../../utils/helpers";

export const loginWithPassword = createAsyncThunk('app/login', async (payload, { getState, rejectWithValue, fulfillWithValue }) => {
   try {
       const { email, password } = payload
       const response = await signInWithEmailAndPassword(FirebaseAuth, email, password);
       console.log("response :", response);
       return fulfillWithValue({
           success: true,
           response
       })
   } catch (error) {
       console.log("Error :", error);
       const errorCode = error.code;
       if (errorCode == 'auth/user-not-found') {
           showCustomToast("User not found.Please do sign up", toastType.e)
       }
       else if (errorCode == 'auth/wrong-password') {
           showCustomToast("Password is not correct", toastType.e)
       }
       else {
           showCustomToast("Something went wrong", toastType.e)
       }
       return rejectWithValue({
           success: false,
           error,
       })
   }
})

26. So now this redux thunk actions (loginWithPassword) auto generates 3 action types. First, to manage the pending state before calling an asynchronous function, and second, to handle data when we got successfully from asynchronous operations.

And last for the rejected state if it fails from the asynchronous process. In the Previous step, we implemented the basic flow. Now we will go with more expansion.

export const loginSlice = createSlice({
   name: "login",
   initialState: {
       loading: false,
       success: false,
       error: null,
   },
   extraReducers: (builder) => {
       builder.addCase(loginWithPassword.pending, (state, action) => {
           state.loading = true;
           state.success = false;
           state.error = null;
       })
       builder.addCase(loginWithPassword.fulfilled, (state, action) => {
           console.log("action :", action);
           state.loading = false;
           if (action?.payload?.success) {
               state.success = true;
               state.error = null;
           }
           else {
               state.success = false;
               state.error = action?.payload?.error;
           }
       })
       builder.addCase(loginWithPassword.rejected, (state, action) => {
           console.log("action :", action);
           state.loading = false;
           state.success = false;
           state.error = action?.payload;
       })
   }
})

So when an asynchronous action fails, it will go with the loginWithPassword.rejected case, and when it successfully passes, it will go with loginWithPassword.fulfilled case. So now we will use this thunk action with our component to go ahead.

27. In your component, you have to call this async thunk action like the given below. So we can dispatch an async thunk function, and after that, it will manage the state after the response comes from API.

const [email, setEmail] = useState("")
   const [password, setPassword] = useState("");
   const dispatch = useDispatch();

   const onClickLogin = () => {

       dispatch(loginWithPassword({
           email, password
       }))

   }

28. If we want to handle results based on thunk response, we can also because thunk always returns results with promises of fulfilled or rejected values, as we implemented previously. So we can do the same kind of thing in our component also.

The dispatched thunk’s promise contains an unwrap property that can be used to throw either the error or, if available, the payload provided by rejectWithValue from a rejected action or to retrieve the payload of a fulfilled action. We can do it like the given below:

  dispatch(loginWithPassword({
               email, password
           })).unwrap()
               .then((originalPromiseResult) => {
                   console.log("originalPromiseResult:", originalPromiseResult);
                   // handle result here
                   if (originalPromiseResult.success) {
                       localStorage.setItem(isLogin, "1")
                       navigate('/dashboard', { replace: true })
                   }
               })
               .catch((rejectedValueOrSerializedError) => {
                   console.log("originalPromiseResult:", rejectedValueOrSerializedError);
                   // handle error here
               })

Code Repo Link: Click here

coma

Conclusion

So Here I have explained how we can implement the redux toolkit with react js. This Redux Toolkit is a very useful concept to make our app more productive and executed smoothly.

It also contains a redux thunk module, so we can do all things using one module, and also, we do not need to create extra files for reducer, action, and action types. So We can implement it in Production and live apps also.

Ronak K

React-Native Developer

Ronak is a React-Native developer with more than 4 years of experience in developing mobile applications for Android and iOS. He is dedicated to his work and passion. He worked in different kinds of application industries that have responsive designs. He also has knowledge of how to make an app with expo cli and react-native web and some basic understanding of react.js.

Keep Reading

Keep Reading

Struggling with EHR integration? Learn about next-gen solutions in our upcoming webinar on Mar 6, at 11 AM EST.

Register Now

Let's create something together!