React 17: Building the Single Page Application using React-Router-DOM Version 6+

In this article, we will implement a Sing-Page-Application using React.js Version 17 and React-Router-DOM Version 6. The React is a JavaScript library for building user interfaces. The React is a great library but when we need to develop a Single-Page-Application (SPA), then we need to rely on the React-Router-DOM, a third-party library. 

React-Router-DOM

This library is specially written for implementing routing features in the React.js application. The current version of this library is 6.x.x. (In the current project the version 6.2.1 is used). Following are some of the objects provided in React-Router-DOM

  1. BrowserRouter: This is used in web browsers. This provides the cleanest URLs.
  2. Routes: This object is used to define Routes. This is a collection of the Route object. The Routes is a container for the nested tree of elements that renders the element based on the Route match.
  3. Route: This object is used to define a Route or we can also call it an expression for rendering the element. This object contains the following properties
    1. path: The URL. This is the expression based on which the Browser Routing will take place.
    2. element: This is a property that represents an element that will be rendered based on URL match. The element accepts the React Component as the input value.
  4. Outlet: This object is used to render the child elements 
  5. Link: The Link for the routing. This object has the 'to' property that accepts the URL as an input value. This value must match with the 'path' value of the Route object.       
Along with the above objects, the React-Router-DOM also provides hooks, some of the hooks are provided below  
  1. useNavigate(), this is used to provide explicit navigation based on events raised on the rendered element. This returns an imperative method for changing (or navigating) to the other element based on the URL passed to it.
  2. useParams(): This hook is used when the parameterized routing is used. This returns an object of key/values pair of dynamic parameters from the current navigated URL.   
The current react project is created using create-react-app CLI. Open the command prompt (or the terminal window if you are using Linux or macOS) and run the following command to install the create-react-app
 
npm install -g create-react-app 

To create the React application run the following command

create-react-app my-react-router

This command will create my-react-router project with the necessary react dependencies installed in it. Let's install the react-router-dom, axios, and boostrap packages for the current application using the following command

npm install --save react-router-dom bootstrap

Step 1: To show the data in the react application, an HTTP request to REST API using made axios object. (I already have the REST API deployed on Azure Cloud. You can use the REST API in your environment). Add a new folder in the src folder of the application and name it as services. In this folder add a new file and name it product-service.js. In this file, we will add the code of accessing data from REST API as shown in listing 1

import axios from 'axios';

class ProductHttpService {
    
    constructor(){
        this.url = "[YOUR-REST-API-URL-HERE]";
    }

    async getData() {
        let resp = await axios.get(`${this.url}/api/Products`);
        return resp;
    }

    async getDataById(id) {
        let resp = await axios.get(`${this.url}/api/Products/${id}`);
        return resp;
    }

    async postData(prd) {
        let resp = await axios.post(`${this.url}/api/Products`, prd, {
            'Content-Type': 'application/json'
        });
        return resp;
    }

    async putData(id,prd) {
        let resp = await axios.put(`${this.url}/api/Products/${id}`, prd, {
            'Content-Type': 'application/json'
        });
        return resp;
    }

    async deleteData(id) {
        let resp = await axios.delete(`${this.url}/api/Products/${id}`);
        return resp;
    }
}

export default ProductHttpService;


Listing 1: Call REST API using axios 
In the ProductHttpService class, since the axios object has the get(), post(), put(), and delete() method returns the Promise object all methods in the class are asynchronous methods.

Step 2: In the src folder add a new folder and name it as components. In this folder, we will add all components. Add a new file in this folder and name it as listproductscomponent.js. This component will use the ProductHttpService class to receive and display all products received from the REST API call in the HTML table. The component uses the products state object that will be used to store all products received from the REST API and generate an HTML table. The code for this component is shown in listing 2


import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import ProductHttpService from "./../services/product-service";
const ListProductsComponent = () => {
  const [products, setProducts] = useState([]);
  const [message, setMessage] = useState("");
  const serv = new ProductHttpService();

  useEffect(() => {
    async function getData() {
      try {
        let response = await serv.getData();
        setProducts(response.data);
      } catch (e) {
        setProducts(`Error Occurred while fetching data ${e.message} `);
      }
    }
    getData();
  }, []);

  const deleteProduct = async (id) => {
    let response = await serv.deleteData(id);
    if (response) {
      setMessage(`Product Deleted Successfully`);
    } else {
      setMessage(`Product Deletion failed`);
    }
  };

  if (products.length === 0) {
    return <div>No Records to display</div>;
  } else {
    return (
      <div className="container">
        <div className="container">
            <strong>{message}</strong>
        </div> 
        <table className="table table-bordered table-striped">
          <thead>
            <tr>
              {Object.keys(products[0]).map((c, i) => (
                <th key={i}>{c}</th>
              ))}
              <th></th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {products.map((prd, idx) => (
              <tr key={idx}>
                {Object.keys(products[0]).map((c, i) => (
                  <td key={i}>{prd[c]}</td>
                ))}
                <td>
                  <button type="button" className="btn btn-warning">
                    <Link to={`/edit/${prd.ProductRowId}`}>Edit</Link>
                  </button>
                </td>
                <td>
                  <input
                    type="button"
                    value="Delete"
                    className="btn btn-danger"
                    onClick={() => deleteProduct(prd.ProductRowId)}
                  />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
};

export default ListProductsComponent;
Listing 2: The ListProductsComponent

The ListProductsComponent, uses the useEffect() hook to call the getData() method from the ProductHttpService class. Once the data is received it will be added to the products state property. The HTML table is generated based on products. This HTML table has the button element which contains Link object from the react-router-dom. This object is used to navigate to the edit link to render the EditProductComponent when the button is clicked. The delete button in the HTML table will be used to delete the product based on the ProductRowId.     

Step 3: In the components folder add a new file and name it as createproductcomponent.js. This file will have a code for CreateProductComponent. This component will contain UI for adding the new product. The code for the component is shown in listing 3
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import ProductHttpService from "../services/product-service";

const CreateProductComponent = () => {
  const [product, setProduct] = useState({
    ProductRowId: 0,
    ProductId: "",
    ProductName: "",
    Manufacturer: "",
    CategoryName: "",
    Description: "",
    BasePrice: 0,
  });
  const [message, setMessage] = useState("");
  const serv = new ProductHttpService();
  const categories = ["Electronics", "Electrical", "Home Appliances"];
  const manufacturers = [
    "MS-Electrical",
    "LS-Home Appliances",
    "LS-Electrical",
    "MS-Electronics",
    "LS-Electronics",
  ];

  let navigate = useNavigate();
  const clear = () =>
    setProduct({
      ProductRowId: 0,
      ProductId: "",
      ProductName: "",
      CategoryName: "",
      Manufacturer: "",
      Description: "",
      BasePrice: 0,
    });

  const save = async () => {
    try {
      let response = await serv.postData(product);
      setProduct(response.data);
      setMessage("Record Added Successfully");
      navigate("/");
    } catch (e) {
      setMessage(`Error Occurred ${e.message}`);
    }
  };

  return (
    <div className="container">
      <h2>Create the New Product</h2>
      <div className="container">
        <strong>{message}</strong>
      </div>
      <div className="form-group">
        <label>Product Row Id</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductRowId}
          readOnly
        />
      </div>
      <div className="form-group">
        <label>Product Id</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductId}
          onChange={(evt) => {
            setProduct({ ...product, ProductId: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Product Name</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductName}
          onChange={(evt) => {
            setProduct({ ...product, ProductName: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Category Name</label>

        <select
          type="text"
          className="form-control"
          name="CategoryName"
          value={product.CategoryName}
          onChange={(evt) => {
            setProduct({ ...product, CategoryName: evt.target.value });
          }}
        >
          <option>Select Category Name</option>
          {categories.map((v, i) => (
            <option key={i} value={v}>
              {v}
            </option>
          ))}
        </select>
      </div>
      <div className="form-group">
        <label>Manufacturer</label>
        <select
          type="text"
          className="form-control"
          value={product.Manufacturer}
          onChange={(evt) => {
            setProduct({ ...product, Manufacturer: evt.target.value });
          }}
        >
          <option>Select Manufacturer</option>
          {manufacturers.map((v, i) => (
            <option key={i} value={v}>
              {v}
            </option>
          ))}
        </select>
      </div>
      <div className="form-group">
        <label>Description</label>
        <input
          type="text"
          className="form-control"
          value={product.Description}
          onChange={(evt) => {
            setProduct({ ...product, Description: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Base Price</label>
        <input
          type="text"
          className="form-control"
          value={product.BasePrice}
          onChange={(evt) => {
            setProduct({ ...product, BasePrice: parseInt(evt.target.value) });
          }}
        />
      </div>
      <div className="form-group">
        <input
          type="button"
          value="Clear"
          className="btn btn-warning"
          onClick={clear}
        />
        <input
          type="button"
          value="Save"
          className="btn btn-success"
          onClick={save}
        />
      </div>
    </div>
  );
};

export default CreateProductComponent;

Listing 3: The CreateProductComponent

Note that the CreateProductComponent, uses the ProductHttpService class to call the postData() method which creates a new product.  The component uses useNavigate() hook to navigate to the 'default' URL (/) when the save() method successfully posts the product data.  

Step 4: In the components folder add a new file and name it as editproductcomponent.js. This file will contain code for the EditProductComponent. Listing 4 show the code for the EditProductComponent 
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import ProductHttpService from "../services/product-service";

const EditProductComponent = () => {
  const [product, setProduct] = useState({
    ProductRowId: 0,
    ProductId: "",
    ProductName: "",
    Manufacturer: "",
    CategoryName: "",
    Description: "",
    BasePrice: 0,
  });
  const [message, setMessage] = useState("");
  const serv = new ProductHttpService();
  const categories = ["Electronics", "Electrical", "Home Appliances"];
  const manufacturers = [
    "MS-Electrical",
    "LS-Home Appliances",
    "LS-Electrical",
    "MS-Electronics",
    "LS-Electronics",
  ];
  let navigate = useNavigate();
  let params = useParams();
  let id = parseInt(params.productRowId);
  useEffect(() => {
    async function getData() {
      try{
        let response = await serv.getDataById(id);
        setProduct(response.data);
      }catch(e){
        setMessage(`Error Occurred ${e.message}`);
      }  
    }
    getData();
  }, []);

  const clear = () =>
    setProduct({
      ProductRowId: 0,
      ProductId: "",
      ProductName: "",
      CategoryName: "",
      Manufacturer: "",
      Description: "",
      BasePrice: 0,
    });

  const save = async () => {
    try {
        let response = await serv.putData(product.ProductRowId, product);
        setProduct(response.data);
        navigate("/");
    }catch(e){
        setMessage(`Record Update failed ${e.message}`);
    }  
   
  };

  return (
    <div className="container">
      <h2>Edit the New Product</h2>
      <div className="container">
        <strong>{message}</strong>
      </div>
      <div className="form-group">
        <label>Product Row Id</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductRowId}
          readOnly
        />
      </div>
      <div className="form-group">
        <label>Product Id</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductId}
          onChange={(evt) => {
            setProduct({ ...product, ProductId: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Product Name</label>
        <input
          type="text"
          className="form-control"
          value={product.ProductName}
          onChange={(evt) => {
            setProduct({ ...product, ProductName: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Category Name</label>

        <select
          type="text"
          className="form-control"
          name="CategoryName"
          value={product.CategoryName}
          onChange={(evt) => {
            setProduct({ ...product, CategoryName: evt.target.value });
          }}
        >
          <option>Select Category Name</option>
          {categories.map((v, i) => (
            <option key={i} value={v}>
              {v}
            </option>
          ))}
        </select>
      </div>
      <div className="form-group">
        <label>Manufacturer</label>
        <select
          type="text"
          className="form-control"
          value={product.Manufacturer}
          onChange={(evt) => {
            setProduct({ ...product, Manufacturer: evt.target.value });
          }}
        >
          <option>Select Manufacturer</option>
          {manufacturers.map((v, i) => (
            <option key={i} value={v}>
              {v}
            </option>
          ))}
        </select>
      </div>
      <div className="form-group">
        <label>Description</label>
        <input
          type="text"
          className="form-control"
          value={product.Description}
          onChange={(evt) => {
            setProduct({ ...product, Description: evt.target.value });
          }}
        />
      </div>
      <div className="form-group">
        <label>Base Price</label>
        <input
          type="text"
          className="form-control"
          value={product.BasePrice}
          onChange={(evt) => {
            setProduct({ ...product, BasePrice: parseInt(evt.target.value) });
          }}
        />
      </div>
      <div className="form-group">
        <input
          type="button"
          value="Clear"
          className="btn btn-warning"
          onClick={clear}
        />
        <input
          type="button"
          value="Save"
          className="btn btn-success"
          onClick={save}
        />
      </div>
    </div>
  );
};

export default EditProductComponent;


Listing 4: The EditProductComponent    

The EditProductComponent contains code similar to CreateProductComponent. The EditProductComponent uses the useNavigate() and useParama() hooks. The useParams() hook is used here to load the product data that is to be updated based on which the navigation is taken place from the ListProductComponent. The useEffect() hook is used to call the getDataById() method from the ProductHttpService class by passing the id parameter which is read from the URL using the useParams() hook. The save() method calls the putData() method of the ProductHttpService class to update the product. If the update operation is successful the navigate will take place to the default ('/') using the useNavigate() hook.

Step 5: In the components folder add a new file and name it as notfound.js. In this file, we will add the NotFound component. This component will be navigated when the route URL does not match. Listing 5 shows the code for the NotFoundComponent

const NotFoundComponent=()=>{
    return (
        <div className="container">
            The Resource You Are Looking For is Not Found 
        </div>
    );
};

export default NotFoundComponent;

Listing 5: NotFoundComponent   

Step 6: In the components folder add a new file and name it as layout.js. In this file, we will create a LayoutComponent.  In this component, we will define Links for the route navigation as shown in listing 6
  

import { Routes, Route, Outlet, Link } from "react-router-dom";
const LayoutComponent=()=>(

  <div className="container">
       <table className="table table-bordered table-striped">
         <tbody>
           <tr>
             <td>
               <Link to='/'>List Products</Link>
             </td>
             <td>
               <Link to='/create'>Create Product</Link>
             </td>
           </tr>
         </tbody>
      </table>
      <hr/>
      <Outlet/>
  </div>
);

export default LayoutComponent;
Listing 6: The LayoutComponent

It is recommended that we should have a separate component to define Link layouts. The Outlet will be used to render the elements based on the route URL match. The Link uses the 'to'  property to set the Route URL to navigate to.  

Step 7:  Let's modify the code of the App.js to define Route URL to various elements as shown in listing 7


import { Routes, Route, Outlet, Link } from "react-router-dom";
import "./App.css";
import ListProductsComponent from "./components/listproductscomponent";
import CreateProductComponent from "./components/createproductcomponent";
import EditProductComponent from "./components/editproductcomponent";
import NotFoundComponent from "./components/notfound";
import LayoutComponent from "./components/layout";
function App() {
  return (
    <div className="container">
      <Routes>
        <Route path="/" element={<LayoutComponent />}>
          <Route index element={<ListProductsComponent />}/>
          <Route path="/create" element={<CreateProductComponent />} />
          <Route path="/edit/:productRowId" element={<EditProductComponent />} />
          <Route path="*" element={<NotFoundComponent />} />
        </Route>
      </Routes>
    </div>
  );
}

export default App;


Listing 7: The App.js to define Rout  

The code in Listing 7 shows the routes. We have imported all the components create in the previous steps. The route for the LayoutComponent has defines all other components as its children. The children components will be rendered in the LayoutComponent based on the Route URL match when the Link is clicked. The Route path '*' means that, if the route URL does not match then it will navigate to the LayoutComponent. The index attribute for the Route of the ListProductComponent means that this component will be by default rendered in the LayoutComponent. The Route for the EditProductComponent has the parameterized route as /edit/:productRowId. This means that to navigate to the EditProductComponent, the URL parameter must be passed. The React-Router-DOM V6 provides the useParams() hook to read the URL parameter.   

Step 8: Modify the index.js to define the BrowserRouter for clean URL in browser as shown in listing 8



import "./../node_modules/bootstrap/dist/css/bootstrap.min.css";
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);
Listing 8: Usig BrowserRouter   

Run the appliction from the Command Prompt using the following command

npm run start

The Browser will be loaded with URL as http://localhost:3000 and the result will be displayed as shown in the figure 1


Figure 1: The Result 

Click on the Create Product Link, the Create Product Componnt will be loaded as shown in  figure 2



Figure 2:  Create Product

Enter the Products details and click on the Save button. If the Save is successful then the navigation will take place to List Products. On the Products List click on the Edit button on any row, the Edit Product view will be loaded as shown in figure 3


Figure 3: The Edit Product 

If you directly change the URL to some some value e.g. http://localhost:3000/gggg, since there is no URL link for gggg is available, the NotFundComponent is loaded as shown in figure 4



Figure 4: NotFundComponent

The code for this article can be found at this link.

Conclusion: The REact-Router-DOM V6 makes the Routing implementation in React Application easier using the hooks.

Comments

Popular posts from this blog

ASP.NET Core 6: Adding Custom Middleware and Logging the Error Message in Database

gRPC Services using .NET 5: Using Entity Framework Core with gRPC for Performing Database Operations

Passing Data Across Blazor Components