Simplified Sample Application: Building with Redux Toolkit, React.js, and ASP.NET Core

22 minute read

Introduction

Redux is a popular state management library for JavaScript applications. It is commonly used with front-end frameworks like React, Angular, and Vue, but can also be used with other frameworks or even vanilla JavaScript. At its core, Redux provides a predictable state container for managing the state of an application. It helps you manage the data that needs to be shared across different components in a more organized and efficient way. The main concept behind Redux is that the entire state of an application is stored in a single JavaScript object, known as the “store.” The Redux store is immutable, which means that you cannot directly modify the state. Instead, to make changes to the state, you dispatch actions. An action is a plain JavaScript object that describes what should change in the state. These actions are processed by pure functions called “reducers,” which take the current state and the dispatched action and return a new state. The unidirectional data flow in Redux ensures that the state changes are predictable and easy to debug, as every state mutation is explicitly defined by actions and reducers.

Redux Toolkit

Redux Toolkit is an official package from the Redux team that simplifies and streamlines the process of working with Redux. It provides a set of utilities and best practices to make Redux development more efficient, maintainable, and less boilerplate-heavy. Before Redux Toolkit, working with Redux required writing a significant amount of code to set up the store, define actions, and write reducers. Redux Toolkit abstracts away much of this boilerplate code and provides a more intuitive and standardized way of using Redux.

Key features of Redux Toolkit include:

  • Configuration Simplicity: Redux Toolkit provides a function called configureStore that creates a Redux store with default configurations. It includes sensible defaults for setting up the store, applying middleware, and enabling Redux DevTools for debugging.
  • Reduces Boilerplate: With Redux Toolkit, you can define “slice” reducers using the createSlice function. A slice is a single part of the Redux store that includes both the reducer and its related actions. This reduces the need to write separate action creators and action types manually.
  • Immutability and Immer Integration: Redux Toolkit leverages the immer library to handle state immutability in a more convenient way. This allows you to write reducers that look like they are directly mutating the state while Redux Toolkit takes care of creating a new immutable state behind the scenes.
  • Thunk Simplification: Redux Toolkit provides the createAsyncThunk function to simplify the creation of thunks for handling asynchronous actions. It makes it easier to write async logic, such as API calls, by dispatching pending, fulfilled, and rejected actions automatically.
  • Easy Migration: If you already have an existing Redux application, Redux Toolkit provides a smooth migration path. It offers compatibility with standard Redux code, allowing you to gradually transition your codebase to the new approach.

Objective:
In this article I will show you how to build a front-end application using redux toolkit with react and asp.net core. Here I am going to create a backend application and a front-end application. Backend application is simple asp.net core application which exposes API. Front end application consumes the api, perform CRUD operation and manage state using redux toolkit. Let’s start.

Tools and Technology Used

  1. ASP.net core Web API
  2. Visual C#
  3. React.js
  4. Redux Toolkit
  5. Axios
  6. Bootstrap
  7. react-toastify

Step 1: Create a asp.net core web api project name Ecommerce.API

Step 2: Install the following nuget packages in the project.

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design

Step 3: Create two Model class name Product and Customer in Models folder.

Product.cs

namespace ECommerce.API.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Description { get; set; }
        public decimal Price { get; set; }
        public int StockQuantity { get; set; }
    }
}

Customer.cs


namespace ECommerce.API.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public string? Email { get; set; }
        public string? Phone { get; set; }
        public DateTime BirthDate { get; set; }

    }
}


Step 4: Create a Context class name ECommerceContext for data access in Db folder.

ECommerceContext.cs

using ECommerce.API.Models;
using Microsoft.EntityFrameworkCore;

namespace ECommerce.API.Db
{
public class ECommerceContext : DbContext
{
public ECommerceContext(DbContextOptions<ECommerceContext> options)
: base(options)
{

        }
        public DbSet<Product> Products { get;set; }
        public DbSet<Customer> Customers { get; set; }
    }

}

Step 5: Create a Seed Generator class name SeedGenerator in Db folder.

SeedGenerator.cs

using ECommerce.API.Models;

namespace ECommerce.API.Db
{
    public class SeedGenerator
    {
        public static void SeedData(WebApplication app)
        {
            using (var scope = app.Services.CreateScope())
            {
                var context = scope.ServiceProvider.GetService<ECommerceContext>();


                if (!context.Products.Any())
                {

                    context.Products.AddRange(
                        new Product
                        {
                            Name = "Smartphone",
                            Description = "A powerful and sleek smartphone with advanced features.",
                            Price = 699.99m,
                            StockQuantity = 50
                        },
                        new Product
                        {
                            Name = "Laptop",
                            Description = "A high-performance laptop for both work and entertainment.",
                            Price = 1299.99m,
                            StockQuantity = 25
                        },
                        new Product
                        {
                            Name = "Wireless Earbuds",
                            Description = "Premium wireless earbuds with noise-canceling technology.",
                            Price = 149.99m,
                            StockQuantity = 100
                        },
                        new Product
                        {
                            Name = "Smart Watch",
                            Description = "A stylish smartwatch with fitness tracking and app notifications.",
                            Price = 199.99m,
                            StockQuantity = 30
                        });
                }


                if (!context.Customers.Any())
                {
                    context.Customers.AddRange(
                        new Customer
                        {
                            Id = 1,
                            FirstName = "John",
                            LastName = "Doe",
                            Email = "john@example.com",
                            Phone = "555-1234",
                            BirthDate = new DateTime(1990, 5, 15)
                        },
                        new Customer
                        {
                            Id = 2,
                            FirstName = "Jane",
                            LastName = "Smith",
                            Email = "jane@example.com",
                            Phone = "555-5678",
                            BirthDate = new DateTime(1985, 8, 22)
                        },
                        new Customer
                        {
                            Id = 3,
                            FirstName = "Michael",
                            LastName = "Johnson",
                            Email = "michael@example.com",
                            Phone = "555-9876",
                            BirthDate = new DateTime(1992, 10, 10)
                        });
                }
                    context.SaveChanges();

            }
        }
    }
}


Step 6: Configure Program class as follows.

Program.cs


using ECommerce.API.Db;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddDbContext<ECommerceContext>(opt => opt.UseInMemoryDatabase("ECommerceDB"));

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

//For seeding data
SeedGenerator.SeedData(app);

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();

    // Enable CORS
    app.UseCors(x => x
    .AllowAnyMethod()
    .AllowAnyHeader()
    .SetIsOriginAllowed(origin => true) // allow any origin
    .AllowCredentials()); // allow credentials
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Step 7: Create two Controller class name ProductsController and CustomersController in Controllers folder.

ProductsController.cs

using ECommerce.API.Db;
using ECommerce.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ECommerce.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ECommerceContext _context;

        public ProductsController(ECommerceContext context)
        {
            _context = context;
        }

        // GET: api/Products
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
        {
          if (_context.Products == null)
          {
              return NotFound();
          }
            return await _context.Products.ToListAsync();
        }

        // GET: api/Products/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetProduct(int id)
        {
          if (_context.Products == null)
          {
              return NotFound();
          }
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            return product;
        }

        // PUT: api/Products/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutProduct(int id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ProductExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Products
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<Product>> PostProduct(Product product)
        {
          if (_context.Products == null)
          {
              return Problem("Entity set 'ECommerceContext.Products'  is null.");
          }
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetProduct", new { id = product.Id }, product);
        }

        // DELETE: api/Products/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            if (_context.Products == null)
            {
                return NotFound();
            }
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool ProductExists(int id)
        {
            return (_context.Products?.Any(e => e.Id == id)).GetValueOrDefault();
        }
    }
}

CustomersController.cs


using ECommerce.API.Db;
using ECommerce.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ECommerce.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly ECommerceContext _context;

        public CustomersController(ECommerceContext context)
        {
            _context = context;
        }

        // GET: api/Customers
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Customer>>> GetCustomers()
        {
          if (_context.Customers == null)
          {
              return NotFound();
          }
            return await _context.Customers.ToListAsync();
        }

        // GET: api/Customers/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Customer>> GetCustomer(int id)
        {
          if (_context.Customers == null)
          {
              return NotFound();
          }
            var customer = await _context.Customers.FindAsync(id);

            if (customer == null)
            {
                return NotFound();
            }

            return customer;
        }

        // PUT: api/Customers/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutCustomer(int id, Customer customer)
        {
            if (id != customer.Id)
            {
                return BadRequest();
            }

            _context.Entry(customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Customers
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<Customer>> PostCustomer(Customer customer)
        {
          if (_context.Customers == null)
          {
              return Problem("Entity set 'ECommerceContext.Customers'  is null.");
          }
            _context.Customers.Add(customer);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetCustomer", new { id = customer.Id }, customer);
        }

        // DELETE: api/Customers/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteCustomer(int id)
        {
            if (_context.Customers == null)
            {
                return NotFound();
            }
            var customer = await _context.Customers.FindAsync(id);
            if (customer == null)
            {
                return NotFound();
            }

            _context.Customers.Remove(customer);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        [HttpGet("SearchCustomers")]
        public async Task<ActionResult<IEnumerable<Customer>>> SearchCustomers(string name)
        {
            if (_context.Customers == null)
            {
                return NotFound();
            }
            List<Customer> customers;
            if (name != "all")
            {
                customers = await _context.Customers.Where(p => p.FirstName.Contains(name) || p.LastName.Contains(name)).ToListAsync();
            }
            else
            {
                customers = await _context.Customers.ToListAsync();
            }
            return customers;
        }

        private bool CustomerExists(int id)
        {
            return (_context.Customers?.Any(e => e.Id == id)).GetValueOrDefault();
        }
    }
}

Now our back-end application is ready. You can run and check the application using swagger.

Step 8: Create a react application name - redux-app. Run the following command in your power shell terminal.

npx create-react-app redux-app

Step 9: Install the following packages in the react project

npm install bootstrap@4.6.0
npm install react-router-dom
npm install react-toastify
npm install react-redux @reduxjs/toolkit
npm install axios

Step 10: Create Conversion.js file in src->utils folder.

Conversion.js

export const convertDateFormat = (date) => {
  let convertedDate = new Date(date);
  return convertedDate.toDateString();
};

export const ConvertDateISOString = (date) => {
  return new Date(date).toISOString().slice(0, 10);
};

Step 11: Create ToastifyMessage.js file in src->helper folder.

ToastifyMessage.js

import { toast } from "react-toastify";

export const SuccessToastify = (message) => {
  toast.success(message, {
    position: toast.POSITION.TOP_CENTER,
    autoClose: 3000, // auto close after 30 seconds
  });
};

export const WarningTostify = (message) => {
  toast.warning(message, {
    position: toast.POSITION.TOP_RIGHT,
    autoClose: 3000, // auto close after 30 seconds
  });
};

export const ErrorToastify = (message) => {
  toast.error(message, {
    position: toast.POSITION.TOP_RIGHT,
    autoClose: 3000, // auto close after 30 seconds
  });
};

export const InfoToastify = (message) => {
  toast.info(message, {
    position: toast.POSITION.TOP_RIGHT,
    autoClose: 3000, // auto close after 30 seconds
  });
};

Step 12: Configure Http connection using axios

  • Create BaseURL.js in src->config folder to set the base URL for API

BaseURL.js

export const Base_URL = "https://localhost:7288/api";
  • Create http-common.js in src->config folder
    http-common.js
import axios from "axios";
import { Base_URL } from "./BaseURL";

export default axios.create({
  baseURL: Base_URL,
  headers: {
    "Content-type": "application/json",
  },
});

Step 13: Create Service Components

  • Create ProductService.js in src->services folder.

ProductService.js

//ProductDataService to make asynchronous HTTP requests
import http from "../config/http-common";

const getAll = () => {
  return http.get("/Products");
};

const get = (id) => {
  return http.get(`/Products/${id}`);
};

const create = (data) => {
  return http.post("/Products", data);
};

const update = (id, data) => {
  return http.put(`/Products/${id}`, data);
};

const remove = (id) => {
  return http.delete(`/Products/${id}`);
};
const ProductDataService = {
  getAll,
  get,
  create,
  update,
  remove,
};

export default ProductDataService;
  • Create CustomerService.js in src->services folder.

CustomerService.js

//CustomerDataService to make asynchronous HTTP requests
import http from "../config/http-common";

const getAll = () => {
  return http.get("/Customers");
};

const get = (id) => {
  return http.get(`/Customers/${id}`);
};

const create = (data) => {
  return http.post("/Customers", data);
};

const update = (id, data) => {
  return http.put(`/Customers/${id}`, data);
};

const remove = (id) => {
  return http.delete(`/Customers/${id}`);
};

const findByName = (name) => {
  return http.get(`/Customers/SearchCustomers?name=${name}`);
};

const CustomerDataService = {
  getAll,
  get,
  create,
  update,
  remove,
  findByName,
};

export default CustomerDataService;

Step 14: Configure Redux

  • Create Slice, Reducer and Actions : Instead of creating many folders and files for Redux (actions, reducers, types,…), with redux-toolkit we just need all-in-one: slice. A slice is a collection of Redux reducer logic and actions for a single feature.

To create a slice, you only need to define the following:

  1. A name to identify the slice.
  2. The initial state value.
  3. One or more reducer functions to specify how the state can be updated.

Once you’ve created a slice, you can effortlessly export the generated Redux action creators and the reducer function for the entire slice. Redux Toolkit simplifies this process with the createSlice() function, which automatically generates action types and action creators based on the reducer function names you provide.

  • Create slice for Product state. Create productsSlice.js in src->redux->slice folder.

productsSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import ProductDataService from "../../services/ProductService";

const initialState = [];

export const createProduct = createAsyncThunk(
  "products/create",
  async (inputData) => {
    const res = await ProductDataService.create(inputData);
    return res.data;
  }
);

export const retrieveProducts = createAsyncThunk(
  "products/retrieve",
  async () => {
    const res = await ProductDataService.getAll();
    return res.data;
  }
);

export const updateProduct = createAsyncThunk(
  "products/update",
  async ({ id, data }) => {
    debugger;
    const res = await ProductDataService.update(id, data);
    return res.data;
  }
);

export const deleteProduct = createAsyncThunk("products/delete", async (id) => {
  debugger;
  await ProductDataService.remove(id);
  return { id };
});

const productSlice = createSlice({
  name: "product",
  initialState,
  extraReducers: {
    [createProduct.fulfilled]: (state, action) => {
      state.push(action.payload);
    },

    [retrieveProducts.fulfilled]: (state, action) => {
      return [...action.payload];
    },

    [updateProduct.fulfilled]: (state, action) => {
      const index = state.findIndex(
        (tutorial) => tutorial.id === action.payload.id
      );
      state[index] = {
        ...state[index],
        ...action.payload,
      };
    },

    [deleteProduct.fulfilled]: (state, action) => {
      let index = state.findIndex(({ id }) => id === action.payload.id);
      state.splice(index, 1);
    },
  },
});

const { reducer } = productSlice;
export default reducer;
  • Create slice for Customer state. Create customersSlice.js in src->redux->slice folder.

customersSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import CustomerDataService from "../../services/CustomerService";

const initialState = [];

export const createCustomer = createAsyncThunk(
  "customers/create",
  async (inputData) => {
    const response = await CustomerDataService.create(inputData);
    return response.data;
  }
);

export const retrieveCustomers = createAsyncThunk(
  "customers/retrieve",
  async () => {
    debugger;
    const res = await CustomerDataService.getAll();
    return res.data;
  }
);

export const updateCustomer = createAsyncThunk(
  "customers/update",
  async ({ id, data }) => {
    const res = await CustomerDataService.update(id, data);
    return res.data;
  }
);

export const deleteCustomer = createAsyncThunk(
  "customers/delete",
  async (id) => {
    await CustomerDataService.remove(id);
    return { id };
  }
);

export const deleteAllCustomer = createAsyncThunk(
  "customers/deleteAll",
  async () => {
    const res = await CustomerDataService.removeAll();
    return res.data;
  }
);

export const findCustomerByName = createAsyncThunk(
  "customers/findByName",
  async ({ name }) => {
    name = name === null || name === "" ? "all" : name;
    const res = await CustomerDataService.findByName(name);
    return res.data;
  }
);

const customerSlice = createSlice({
  name: "customer",
  initialState,
  extraReducers: {
    [createCustomer.fulfilled]: (state, action) => {
      state.push(action.payload);
    },

    [retrieveCustomers.fulfilled]: (state, action) => {
      return [...action.payload];
    },

    [updateCustomer.fulfilled]: (state, action) => {
      const index = state.findIndex(
        (tutorial) => tutorial.id === action.payload.id
      );
      state[index] = {
        ...state[index],
        ...action.payload,
      };
    },

    [deleteCustomer.fulfilled]: (state, action) => {
      let index = state.findIndex(({ id }) => id === action.payload.id);
      state.splice(index, 1);
    },
    [deleteAllCustomer.fulfilled]: (state, action) => {
      return [];
    },
    [findCustomerByName.fulfilled]: (state, action) => {
      return [...action.payload];
    },
  },
});

// destructure reducer from customerSlice
// name mustbe reducer
const { reducer } = customerSlice;
export default reducer;
  • Create Redux Store in src->redux folder name store.js

Store.js

// This Store will bring Actions and Reducers together and hold the Application state.

import { configureStore } from "@reduxjs/toolkit";
import productReducer from "./slices/productsSlice";
import customerReducer from "./slices/customersSlice";

const reducer = {
  products: productReducer,
  customers: customerReducer,
};

const store = configureStore({
  reducer: reducer,
  devTools: true,
});

export default store;

Note:
The Store is the central entity that combines Actions and Reducers, responsible for managing the entire application state.

With the help of the configureStore() function from Redux Toolkit, you can enjoy the following automated benefits:

  1. It enables the Redux DevTools Extension out of the box, providing powerful debugging capabilities.
  2. The thunk middleware is automatically set up, allowing you to start writing thunks without any additional configuration.

Step 15: Make state available to React components.

  • Modify the index.js as follows. Here, we are wrapping the entire application in a component to make store available to its child components.

Index.js

import "react-toastify/dist/ReactToastify.css";
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";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    {/* Now we want our entire React App to access the store */}
    <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 16: Create a Component for Nav bar name TopNav.js in src->layout folder.

TopNav.js

import React from "react";
import { Link } from "react-router-dom";

const TopNav = () => {
  return (
    <nav className="navbar navbar-expand navbar-dark bg-dark">
      <a href="/products" className="navbar-brand">
        Mahedee.net
      </a>
      <div className="navbar-nav mr-auto">
        <li className="nav-item">
          <Link to={"/products"} className="nav-link">
            Products
          </Link>
        </li>

        <li className="nav-item">
          <Link to={"/customers"} className="nav-link">
            Customers
          </Link>
        </li>
      </div>
    </nav>
  );
};

export default TopNav;

Step 17: Create components for CRUD operation for Product in src->components->product.

  • Create ProductList.js file for ProductList component.

ProductList.js

import { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import {
  deleteProduct,
  retrieveProducts,
} from "../../redux/slices/productsSlice";
import { WarningTostify } from "../../helper/ToastifyMessage";

const ProductList = () => {
  const products = useSelector((state) => state.products);
  const dispatch = useDispatch();

  const initFetch = useCallback(() => {
    dispatch(retrieveProducts());
  }, [dispatch]);

  useEffect(() => {
    initFetch();
  }, [initFetch]);

  function onDeleteProduct(id) {
    debugger;
    dispatch(deleteProduct(id))
      .then((response) => {
        WarningTostify(`Product with id: ${id} has been deleted.`);
      })
      .catch((e) => {
        console.log(e);
      });
  }

  return (
    <div className="card">
      <div className="card-body">
        <div>
          <h3>Product List</h3>
          <Link to={"/add-product/"}>
            <button className="btn btn-primary">Create</button>
          </Link>

          <table className="table table-stripped">
            <thead>
              <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
                <th>Stock Quantity</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {products &&
                products.map((product) => (
                  <tr key={product.id}>
                    <td>{product.id}</td>
                    <td>{product.name}</td>
                    <td>{product.price}</td>
                    <td>{product.stockQuantity}</td>
                    <td>
                      {" "}
                      <Link
                        to={"/edit-product/" + product.id}
                        className="badge badge-warning"
                      >
                        Edit
                      </Link>
                      ||
                      <Link
                        onClick={() => onDeleteProduct(product.id)}
                        className="badge badge-danger"
                      >
                        Delete
                      </Link>
                    </td>
                  </tr>
                ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

export default ProductList;
  • Create AddProduct.js file for AddProduct component.

AddProduct.js

import { useState } from "react";
import { useDispatch } from "react-redux";
import { createProduct } from "../../redux/slices/productsSlice";
import { useNavigate } from "react-router-dom";
import { SuccessToastify } from "../../helper/ToastifyMessage";

const AddProduct = () => {
  const initialProductState = {
    id: 0,
    name: "",
    description: "",
    price: 0,
    stockQuantity: 0,
  };

  const [product, setProduct] = useState(initialProductState);
  const navigate = useNavigate();

  const dispatch = useDispatch();

  const handleInputChange = (event) => {
    const { name, value } = event.target;

    setProduct({ ...product, [name]: value });
  };

  const saveProduct = () => {
    dispatch(createProduct(product))
      .unwrap()
      .then((data) => {
        console.log(data);
        setProduct({
          id: data.id,
          name: data.title,
          description: data.description,
          price: data.price,
          stockQuantity: data.stockQuantity,
        });
        SuccessToastify("Product information saved successfully!!");
        navigate("/products");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const handleBackToList = () => {
    navigate("/products");
  };

  return (
    <div className="card">
      <div className="card-body">
        <div className="row">
          <div className="col-md-3">
            <h3>Create Product</h3>
            <div>
              <div className="form-group">
                <label htmlFor="name">Name</label>
                <input
                  type="text"
                  className="form-control"
                  id="name"
                  required
                  value={product.name || ""}
                  onChange={handleInputChange}
                  name="name"
                />
              </div>
              <div className="form-group">
                <label htmlFor="description">Description</label>
                <input
                  type="text"
                  className="form-control"
                  id="description"
                  required
                  value={product.description || ""}
                  onChange={handleInputChange}
                  name="description"
                />
              </div>
              <div className="form-group">
                <label htmlFor="price">Price</label>
                <input
                  type="text"
                  className="form-control"
                  id="price"
                  required
                  value={product.price || ""}
                  onChange={handleInputChange}
                  name="price"
                />
              </div>
              <div className="form-group">
                <label htmlFor="stockQuantity">Stock Quantity</label>
                <input
                  type="text"
                  className="form-control"
                  id="stockQuantity"
                  required
                  value={product.stockQuantity || ""}
                  onChange={handleInputChange}
                  name="stockQuantity"
                />
              </div>
              <button
                type="button"
                onClick={saveProduct}
                className="btn btn-success"
              >
                Submit
              </button>
              ||
              <button
                type="button"
                className="btn btn-info"
                onClick={handleBackToList}
              >
                Back to List
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default AddProduct;
  • Create EditProduct.js file for EditProduct component.

EditProduct.js

import { useNavigate, useParams } from "react-router-dom";

import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import ProductDataService from "../../services/ProductService";
import { updateProduct } from "../../redux/slices/productsSlice";
import { SuccessToastify } from "../../helper/ToastifyMessage";

const EditProduct = () => {
  const { id } = useParams();
  let navigate = useNavigate();

  const initialProductState = {
    id: 0,
    name: "",
    description: "",
    price: 0,
    stockQuantity: 0,
  };

  const [productObj, setProductObj] = useState(initialProductState);
  const dispatch = useDispatch();

  const getProduct = (id) => {
    ProductDataService.get(id)
      .then((response) => {
        setProductObj(response.data);
      })
      .catch((e) => {
        console.log(e);
      });
  };

  useEffect(() => {
    if (id) {
      getProduct(id);
    }
  }, [id]);

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setProductObj({ ...productObj, [name]: value });
  };

  const updateProductContent = () => {
    dispatch(updateProduct({ id: productObj.id, data: productObj }))
      .unwrap()
      .then((response) => {
        SuccessToastify("Product information has been updated successfully!");
        navigate("/products");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const backtoList = () => {
    navigate("/products");
  };

  return (
    <div className="card">
      <div className="card-body">
        <div className="row">
          <div className="col-md-3">
            <h3>Edit Product</h3>
            <form>
              <div className="form-group">
                <label htmlFor="name">Name</label>
                <input
                  type="text"
                  className="form-control"
                  id="name"
                  name="name"
                  value={productObj.name}
                  onChange={handleInputChange}
                />
              </div>
              <div className="form-group">
                <label htmlFor="description">Description</label>
                <input
                  type="text"
                  className="form-control"
                  id="description"
                  name="description"
                  value={productObj.description}
                  onChange={handleInputChange}
                />
              </div>

              <div className="form-group">
                <label htmlFor="price">Price</label>
                <input
                  type="text"
                  className="form-control"
                  id="price"
                  name="price"
                  value={productObj.price}
                  onChange={handleInputChange}
                />
              </div>

              <div className="form-group">
                <label htmlFor="stockQuantity">Stock Quantity</label>
                <input
                  type="text"
                  className="form-control"
                  id="stockQuantity"
                  name="stockQuantity"
                  value={productObj.stockQuantity}
                  onChange={handleInputChange}
                />
              </div>
            </form>
            <div className="form-group">
              <input
                type="button"
                value="Edit"
                className="btn btn-primary"
                onClick={updateProductContent}
              ></input>
              ||
              <input
                type="button"
                value="Back to List"
                className="btn btn-primary"
                onClick={backtoList}
              ></input>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default EditProduct;

Step 18: Create components for CRUD operation for Customer in src->components->customer.

  • Create CustomerList.js file for CustomerList component.

CustomerList.js

import { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import {
  deleteCustomer,
  findCustomerByName,
  retrieveCustomers,
} from "../../redux/slices/customersSlice";
import { ConvertDateISOString } from "../../utils/Conversion";
import { WarningTostify } from "../../helper/ToastifyMessage";

const CustomerList = () => {
  const [searchCustomer, setSearchCustomer] = useState("");

  const customers = useSelector((state) => state.customers);
  const dispatch = useDispatch();
  const initFetch = useCallback(() => {
    dispatch(retrieveCustomers());
  }, [dispatch]);

  useEffect(() => {
    initFetch();
  }, [initFetch]);

  function onDeleteCustomer(id) {
    dispatch(deleteCustomer(id))
      .then((response) => {
        console.log(response);
        WarningTostify("Customer has been deleted");
      })
      .catch((e) => {
        console.log(e);
      });
  }

  const onChangeSearchCustomer = (e) => {
    const searchCustomer = e.target.value;

    setSearchCustomer(searchCustomer);
  };

  const findByCustomerName = () => {
    dispatch(findCustomerByName({ name: searchCustomer }));
  };

  return (
    <div className="card">
      <div className="card-body">
        <div>
          <h3>Customer List</h3>
          <Link to={"/add-customer/"}>
            <button className="btn btn-primary">Create</button>
          </Link>

          <br></br>
          <br></br>
          <div className="input-group mb-3">
            <input
              type="text"
              placeholder="Search by customer name"
              className="form-control"
              value={searchCustomer}
              onChange={onChangeSearchCustomer}
            />
            <div className="input-group-append">
              <button
                className="btn btn-info "
                type="button"
                onClick={findByCustomerName}
              >
                Search
              </button>
            </div>
          </div>

          <table className="table table-stripped">
            <thead>
              <tr>
                <th>Id</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Email</th>
                <th>Phone</th>
                <th>Birth Date</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {customers &&
                customers.map((customer) => (
                  <tr key={customer.id}>
                    <td>{customer.id}</td>
                    <td>{customer.firstName}</td>
                    <td>{customer.lastName}</td>
                    <td>{customer.email}</td>
                    <td>{customer.phone}</td>
                    <td>{ConvertDateISOString(customer.birthDate)}</td>
                    <td>
                      {" "}
                      <Link
                        to={"/edit-customer/" + customer.id}
                        className="badge badge-warning"
                      >
                        Edit
                      </Link>
                      ||
                      <Link
                        onClick={() => onDeleteCustomer(customer.id)}
                        className="badge badge-danger"
                      >
                        Delete
                      </Link>
                    </td>
                  </tr>
                ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

export default CustomerList;
  • Create AddCustomer.js file for AddCustomer component.

AddCustomer.js

import { useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { createCustomer } from "../../redux/slices/customersSlice";
import { SuccessToastify } from "../../helper/ToastifyMessage";

const AddCustomer = () => {
  const initialCustomerState = {
    id: 0,
    firstName: "",
    lastName: "",
    email: "",
    phone: "",
    birthDate: "",
  };

  const [customer, setCustomer] = useState(initialCustomerState);
  const navigate = useNavigate();

  const dispatch = useDispatch();

  const handleInputChange = (event) => {
    const { name, value } = event.target;

    setCustomer({ ...customer, [name]: value });
  };

  const saveCustomer = () => {
    dispatch(createCustomer(customer))
      .unwrap()
      .then((data) => {
        SuccessToastify("Customer has been created successfully!");
        setCustomer({
          id: data.id,
          firstName: data.firstName,
          lastName: data.lastName,
          email: data.email,
          phone: data.phone,
          birthDate: data.birthDate,
        });
        navigate("/customers");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const backToList = () => {
    navigate("/customers");
  };

  return (
    <div className="card">
      <div className="card-body">
        <div className="row">
          <div className="col-md-3">
            <h3>Create Customer</h3>
            <div>
              <div className="form-group">
                <label htmlFor="firstName">First Name</label>
                <input
                  type="text"
                  className="form-control"
                  id="firstName"
                  required
                  value={customer.firstName || ""}
                  onChange={handleInputChange}
                  name="firstName"
                />
              </div>
              <div className="form-group">
                <label htmlFor="description">Last Name</label>
                <input
                  type="text"
                  className="form-control"
                  id="lastName"
                  required
                  value={customer.lastName || ""}
                  onChange={handleInputChange}
                  name="lastName"
                />
              </div>
              <div className="form-group">
                <label htmlFor="email">Email</label>
                <input
                  type="text"
                  className="form-control"
                  id="email"
                  required
                  value={customer.email || ""}
                  onChange={handleInputChange}
                  name="email"
                />
              </div>
              <div className="form-group">
                <label htmlFor="phone">Phone</label>
                <input
                  type="text"
                  className="form-control"
                  id="phone"
                  required
                  value={customer.phone || ""}
                  onChange={handleInputChange}
                  name="phone"
                />
              </div>
              <div className="form-group">
                <label htmlFor="birthDate">Birth Date</label>
                <input
                  type="date"
                  className="form-control"
                  id="birthDate"
                  required
                  value={customer.birthDate || ""}
                  onChange={handleInputChange}
                  name="birthDate"
                />
              </div>
              <button onClick={saveCustomer} className="btn btn-success">
                Submit
              </button>
              ||
              <button className="btn btn-success" onClick={backToList}>
                Back to List
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default AddCustomer;
  • Create EditCustomer.js file for EditCustomer component.

EditCustomer.js

import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import CustomerDataService from "../../services/CustomerService";
import { updateCustomer } from "../../redux/slices/customersSlice";
import { SuccessToastify } from "../../helper/ToastifyMessage";
import { ConvertDateISOString } from "../../utils/Conversion";

const EditCustomer = () => {
  const { id } = useParams();
  let navigate = useNavigate();

  const initialCustomerState = {
    id: 0,
    firstName: "",
    lastName: "",
    email: "",
    phone: "",
    birthDate: null,
  };

  const [customerObj, setCustomerObj] = useState(initialCustomerState);

  const dispatch = useDispatch();

  const getCustomer = (id) => {
    CustomerDataService.get(id)
      .then((response) => {
        setCustomerObj(response.data);
      })
      .catch((e) => {
        console.log(e);
      });
  };

  useEffect(() => {
    if (id) {
      getCustomer(id);
    }
  }, [id]);

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setCustomerObj({ ...customerObj, [name]: value });
  };

  const backtoList = () => {
    navigate("/customers");
  };

  const onSubmit = () => {
    console.log("Clicked submit button");

    dispatch(updateCustomer({ id: customerObj.id, data: customerObj }))
      .unwrap()
      .then((response) => {
        console.log(response);
        SuccessToastify("Customer has been updated successfully!");
        navigate("/customers");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  return (
    <div className="card">
      <div className="card-body">
        <div className="row">
          <div className="col-md-3">
            <h3>Edit Customer</h3>
            <form>
              <div className="form-group">
                <label className="control-label">First Name: </label>
                <input
                  className="form-control"
                  type="text"
                  id="firstName"
                  name="firstName"
                  value={customerObj.firstName}
                  onChange={handleInputChange}
                ></input>
              </div>

              <div className="form-group">
                <label className="control-label">Last Name: </label>
                <input
                  className="form-control"
                  type="text"
                  id="lastName"
                  name="lastName"
                  value={customerObj.lastName}
                  onChange={handleInputChange}
                ></input>
              </div>

              <div className="form-group">
                <label className="control-label">Email: </label>
                <input
                  className="form-control"
                  type="text"
                  id="email"
                  name="email"
                  value={customerObj.email}
                  onChange={handleInputChange}
                ></input>
              </div>

              <div className="form-group">
                <label className="control-label">Phone: </label>
                <input
                  className="form-control"
                  type="text"
                  id="phone"
                  name="phone"
                  value={customerObj.phone}
                  onChange={handleInputChange}
                ></input>
              </div>

              <div className="form-group">
                <label className="control-label">Birth Date: </label>
                <input
                  className="form-control"
                  type="date"
                  id="birthDate"
                  name="birthDate"
                  value={ConvertDateISOString(customerObj.birthDate)}
                  onChange={handleInputChange}
                ></input>
              </div>
            </form>

            <div className="form-group">
              <input
                type="button"
                value="Edit Customer"
                className="btn btn-primary"
                onClick={onSubmit}
              ></input>
              ||
              <input
                type="button"
                value="Back to List"
                className="btn btn-primary"
                onClick={backtoList}
              ></input>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default EditCustomer;

Step 19: Modify App.js and configure route as follows.

App.js

import logo from "./logo.svg";
import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import EditProduct from "./components/product/EditProduct";
import TopNav from "./layout/TopNav";
import CustomerList from "./components/customer/CustomerList";
import AddCustomer from "./components/customer/AddCustomer";
import EditCustomer from "./components/customer/EditCustomer";
import { ToastContainer } from "react-toastify";
import ProductList from "./components/product/ProductsList";
import AddProduct from "./components/product/AddProduct";

function App() {
  return (
    <BrowserRouter>
      <TopNav />
      <Routes>
        <Route path="/" element={<ProductList />} />
        <Route path="/products" element={<ProductList />} />
        <Route path="/add-product" element={<AddProduct />} />
        <Route path="/edit-product/:id" element={<EditProduct />} />
        <Route path="/customers" element={<CustomerList />} />
        <Route path="/add-customer" element={<AddCustomer />} />
        <Route path="/edit-customer/:id" element={<EditCustomer />} />
      </Routes>
      <ToastContainer />
    </BrowserRouter>
  );
}

export default App;

Step 20: Fixed port to run front-end application.

  • Create a file .env in root directory.
  • Write the following code to fix the port. Now, application will run on 3002 port.

.env

PORT=3002

Step 21: Run front end application using the following command.

npm start

Run backend application and you will see the following screen. Now you can perform CRUD operation for both Product and Customer.

Source code