Integrate Redux Toolkit in Dynamic React.JS Typescript App


On this blog, I'll share to your the Redux Toolkit and integrate it to a dynamic React.JS Typepscript app. I will also give you a brief introduction about ReduxToolkit and how powerful this toolset is.

May 31, 2022
Introduction

In building a big application using React.js, in order to maintain, properly manage and access different state changes across components, developers tend to use state management libraries like React Redux, Recoil, Jotai etc. Most of React.js applications are using React Redux. But having Redux in your React.js brings some concerns like: "Configuring the store is too complicated", "many boilerplates" and "add a lot of packages to get Redux to do anything useful". If you're new in Redux, it would need you to take more time to spend learning the state management. People behind React Redux are aware of this. That's why they introduced a new option to handle your state management with ease. This is Redux Toolkit.

Why Redux Toolkit?

On their site, they described it as their official, batteries-included toolset for efficient Redux development. And it's intended to be the standard way to write Redux logic and they definitely recommend this to use.

Simple - Provides good defaults for store setup out of the box, and includes the most commonly used Redux addons built-in.

Opinionated - Provides good defaults for store setup out of the box, and includes the most commonly used Redux addons built-in.

Powerful - Takes inspiration from libraries like Immer and Autodux to let you write "mutative" immutable update logic, and even create entire "slices" of state automatically.

Effective - Lets you focus on the core logic your app needs, so you can do more work with less code.

source

Alright, after introducing you about Redux Toolkit, I'll now proceed to setting up and usage of our redux toolkit in our component.

Step 1 - Setting up your React.JS project

1. Including or Adding Redux Toolkit in the project.

For new project
          # Redux + Plain JS template
npx create-react-app my-app --template redux
  
# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript
      
Install the redux toolkit package

NPM: npm install @reduxjs/toolkit or YARN:yarn add @reduxjs/toolkit

Optionally, you can install other packages like axios because in our example application, I set up the axios and other packages there for UI.

Step 2 - Create your first slice

A "Slice" in redux is a collection of redux reducer logic and actions. In our example, we created the employeeSlice.ts and it's called the API methods.

        
 import { createSlice } from "@reduxjs/toolkit";
 import { addEmployee, deleteEmployee, getEmployees, updateEmployee } from "./employeeApi";
 
 export const employeeSlice = createSlice({
     name: "employee",
     initialState: {
         list: {
             isLoading: false,
             status: "",
             values: []
         },
         save: {
             isSaving: false,
             isDeleting: false
         }
     },
     reducers: {
         clearSuccessMessage: (state, payload) => {
             // TODO: Update state to clear success message
         }
     },
     extraReducers: {
         [getEmployees.pending.type]: (state, action) => {
             state.list.status = "pending"
             state.list.isLoading = true
         },
         [getEmployees.fulfilled.type]: (state, { payload }) => {
             state.list.status = "success"
             state.list.values = payload
             state.list.isLoading = false
         },
         [getEmployees.rejected.type]: (state, action) => {
             state.list.status = "failed"
             state.list.isLoading = false
         },
         [addEmployee.pending.type]: (state, action) => {
             state.save.isSaving = true
         },
         [addEmployee.fulfilled.type]: (state, action) => {
             state.save.isSaving = false
         },
         [addEmployee.rejected.type]: (state, action) => {
             state.save.isSaving = false
         },
         [updateEmployee.pending.type]: (state, action) => {
             state.save.isSaving = true
         },
         [updateEmployee.fulfilled.type]: (state, action) => {
             state.save.isSaving = false
         },
         [updateEmployee.rejected.type]: (state, action) => {
             state.save.isSaving = false
         },
         [deleteEmployee.pending.type]: (state, action) => {
             state.save.isDeleting = true
         },
         [deleteEmployee.fulfilled.type]: (state, action) => {
             state.save.isDeleting = false
         },
         [deleteEmployee.rejected.type]: (state, action) => {
             state.save.isDeleting = false
         }
     }
 })
 
 export default employeeSlice.reducer
      
Step 3 - Create store.ts

The store.ts is where we configure and register our store and the slices that we need throughout the application.

        
  import { configureStore } from "@reduxjs/toolkit";
  import { useDispatch } from "react-redux";
  import employeeSlice from "./features/Employee/employeeSlice";
  
  export const store = configureStore({
      reducer: {
          employee: employeeSlice
      },
  });
  
  // dispatch does not take types for thunks into account and thus the return type is typed incorrectly. Please use the actual Dispatch type from the store as decsribed in the documentation. Ref: https://stackoverflow.com/questions/63811401/property-then-does-not-exist-on-type-asyncthunkaction-redux-toolkit
  export type AppDispatch = typeof store.dispatch
  export const useAppDispatch = () => useDispatch<AppDispatch>()
  
  // Infer the `RootState` and `AppDispatch` types from the store itself
  export type RootState = ReturnType<typeof store.getState>
      
On the codes above, this is where we register our reducer.

In my example, we're connecting to a web API endpoints and it has endpoints for getting basic employee details.

Step 4 - Configure index.tsx

In the index.tsx, we add the components Provider as the parent component of our App component. This way, we can access the store throughout the app components.

        
  ReactDOM.render(
    <React.StrictMode>
      <Provider store={store}>
        <App />
      </Provider>
    </React.StrictMode>,
    document.getElementById("root")
  );
      
Step 5 - Setup the UI component

For this example, I added some UI design for our single component. In real world scenario, we put the functionalities in separate components, but for the purpose of this blog, we put them into one.

        
  import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { IEmployee, IEmployeeList } from "../../models/employee";
import { RootState, useAppDispatch } from "../../store";
import {
  getEmployees,
  addEmployee,
  updateEmployee,
  deleteEmployee,
} from "./employeeApi";
import moment from "moment";
import { Input, Checkbox, Button } from "../../components";
import { toast, ToastContainer } from "react-toastify";

export const Employee: React.FC = () => {
  const dispatch = useAppDispatch();

  useEffect(() => {
    dispatch(getEmployees());
  }, [dispatch]);

  const employeeList = useSelector(
    (state: RootState) => state.employee.list.values
  );
  const isLoadingTable = useSelector(
    (state: RootState) => state.employee.list.isLoading
  );
  const isSaving = useSelector(
    (state: RootState) => state.employee.save.isSaving
  );
  const isDeleting = useSelector(
    (state: RootState) => state.employee.save.isDeleting
  );

  const [employee, setEmployee] = useState<IEmployee>({
    employeeId: 0,
    name: "",
    birthday: moment(new Date()).format("YYYY-MM-DD"),
    isActive: false,
  });

  const [showValidation, setShowValidation] = useState<boolean>(false);

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value, checked } = e.target;
    setEmployee((prevState) => ({
      ...prevState,
      [name]: name === "isActive" ? checked : value,
    }));
  };

  const selectEmployee = (d: IEmployee) => {
    setShowValidation(false);
    setEmployee({
      employeeId: d.employeeId,
      name: d.name,
      isActive: d.isActive,
      birthday: moment(d.birthday).format("YYYY-MM-DD"),
    });
  };

  const removeEmployee = (id: number) => {
    if (id)
      dispatch(deleteEmployee(id))
        .unwrap()
        .then((response) => {
          toast.success(response);
          dispatch(getEmployees());
        })
        .catch((error) => {
          toast.error(error);
        });
  };

  const submit = (e: React.SyntheticEvent) => {
    e.preventDefault();

    if (employee.name === "") {
      setShowValidation(true);
      return;
    }

    const action =
      employee.employeeId === 0
        ? addEmployee(employee)
        : updateEmployee(employee);

    dispatch(action)
      .unwrap()
      .then((response) => {
        toast.success(response);
        resetForm();
        dispatch(getEmployees());
      })
      .catch((error) => {
        toast.error(error);
      });
  };

  const resetForm = () => {
    setEmployee({
      employeeId: 0,
      name: "",
      isActive: false,
      birthday: moment(new Date()).format("YYYY-MM-DD"),
    });
    setShowValidation(false);
  };

  return (
    <>
      <div className="form-container">
        <h1 className="title">
          Employee &nbsp;
          <span className="tag is-link">{employeeList?.length}</span>
        </h1>
        <div className="card">
          <div className="card-content">
            <div className="content">
              <div className="columns">
                <div className="column is-4">
                  <Checkbox
                    title="Active"
                    name="isActive"
                    value={employee.isActive}
                    inputChange={handleInputChange}
                  />
                </div>
              </div>
              <div className="columns">
                <div className="column is-4">
                  <Input
                    type="text"
                    title="Name"
                    name="name"
                    placeholder="Enter name here"
                    value={employee.name}
                    inputChange={handleInputChange}
                    showValidation={showValidation}
                    isRequired={true}
                  />
                </div>
                <div className="column is-4">
                  <Input
                    type="date"
                    title="Birthday"
                    name="birthday"
                    value={employee.birthday}
                    inputChange={handleInputChange}
                  />
                </div>
              </div>
              <Button
                type="is-success"
                loading={isSaving}
                title="Submit"
                onClick={submit}
                disabled={isSaving || isDeleting}
              />
              &nbsp;
              {employee.employeeId !== 0 && (
                <Button
                  title="Cancel"
                  onClick={resetForm}
                  disabled={isSaving || isDeleting}
                />
              )}
              <hr />
              {isLoadingTable && (
                <div className="has-text-centered">Fetching...</div>
              )}
              <div className="table-container">
                <table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
                  <thead>
                    <tr>
                      <th>Name</th>
                      <th>Active</th>
                      <th>Birthday</th>
                      <th></th>
                    </tr>
                  </thead>
                  <tbody>
                    {employeeList?.map((d: IEmployeeList, index: number) => {
                      return (
                        <tr key={index}>
                          <td>{d.name}</td>
                          <td>{d.isActive ? "Active" : "Inactive"}</td>
                          <td>{moment(d.birthday).format("MM/DD/YYYY")}</td>
                          <td>
                            <Button
                              type="is-warning"
                              title="Edit"
                              onClick={() => selectEmployee(d)}
                              disabled={isSaving || isDeleting}
                            />
                            &nbsp;
                            <Button
                              type="is-danger"
                              title="Delete"
                              loading={isDeleting}
                              onClick={() => removeEmployee(d.employeeId)}
                              disabled={isSaving || isDeleting}
                            />
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            </div>
          </div>
        </div>
        <ToastContainer closeOnClick={true} />
      </div>
    </>
  );
};
      

As you can see in the code above, we can easily access the state store by using useSelector and if we can call a method that's part of our slice, we can just const dispatch = useAppDispatch();


The full example and source code is available in this repository. https://github.com/deanilvincent/React.JS-ReduxToolkit-Typescript-CRUD-Sample

If you have some questions or comments, please drop it below 👇 :)

Buy Me A Tea