Routing in React.js Single Page Application using React-Router-Dom

The compositional pattern of the React.js library makes Single Page Application (SPA) development easier. The most important need of SPA is an implementation of Routing in the app. Since React.js does not have any built-in routing support, we can install the react-router-dom library which provides a set of objects to enable router functionality for React applications. 

The react-router-dom provides some of the following important classes:

1. Router - the common low level interface for all router components. This contains the route expression to the components using the following properties: 
- path -  this property contains the URL used for navigating to a component. 
- component - name of the component to navigate to. 
- exact - when the property value is true, this will navigate only when the path exactly matches the location.pathname . 


2. Redirect - Rendering a <Redirect> will navigate to a new location. The new location will override the current location in the history stack. 
- to - property of Redirect contains the URL to navigate to. 


3. Link  - used to provide declarative and accessible navigation to URL declared in Route. 
- to - property contains the URL, this URL will be used to execute the routing for the application. 


4. Switch - used to render the first child Route or the Redirect that matches to the URL location. 

The first Route child can be the default route which we generally define using '/'. 


Let's implement the application. To implement this application, I have used Microsoft Visual Studio Code which is a free IDE from Microsoft. 


Step 1: Open VS Code and create a new React application using create-react-app CLI by running the following command from the command prompt: 

create-react-app react-app-router 

Running this command will create a new React application. Navigate to the react-app-router application folder on the command prompt and run the following command: 

npm install --save react-router-dom bootstrap axios 

The above command will install the react-router-dom, bootstrap and axios packages in the current application as dependencies. We will use Bootstrap classes to add a rich UI for our React application. The axios package will be used to manage AJAX calls to REST APIs. 


Step 2: 
Modify App.js by adding code in this file as shown in listing 1: 

  
import React from 'react';
import {Route, Link, Switch,Redirect} from 'react-router-dom';
 
import './App.css';
import './../node_modules/bootstrap/dist/css/bootstrap.min.css';
import CreateProductComponent from './components/CreateProductComponent';
import EditProductComponent from './components/EditProductComponent';
import ProductListComponent from './components/ProductListComponent';

function App() {
  return (
  <div className="container">
                <h1>The React Routing Application</h1>
                <table className="table table-bordered table-striped table-danger">
                   
                  <tbody>
                     <tr>
                        <td>
                            <Link to="/">List Products</Link>
                        </td>
                          <td>
                            <Link to="/create">Create Product</Link>
                        </td> 
                     </tr>
                  </tbody>
                </table>
                {/* Define Route Table Here */}
                <div>
                    <Switch>
                        <Route exact path="/" component={ProductListComponent}></Route>
                        <Route exact path="/create" component={CreateProductComponent}></Route>
                        <Route exact path="/edit/:id" component={EditProductComponent}></Route>
                       
                        <Redirect to="/"/>
                    </Switch>
                </div>
            </div>
  
  );
}


Listing 1: App.js showing the Routing configuration 


The code in the listing 1 shows the Route Configuration. The Switch object defines the Routes with path and component properties. The Link object define the declarative navigation access to URLs. 


Step 2: 
In the src folder add a new folder and name this folder as services. In this folder, add a new file and name this file as httpservice.js. In this file add the code for making calls to REST API as shown in listing 2. 

Note: You can create a REST APIs using Node.js + Express, ASP.NET WEB API, ASP.NET Core, etc. 

import axios from 'axios';

export class HttpService {
    constructor(){
        this.url ='https://myserver/api/Products';
    }

    getData() {
        let response = axios.get(this.url);
        return response;
    }
    getDataById(id) {
        let response = axios.get(`${this.url}/${id}`);
        return response;
    }
    postData(prd) {
        let response = axios.post(this.url, prd, {
            'Content-Type' : 'application/json'
        });
        return response;
    }

    putData(prd) {
        let response = axios.put(`${this.url}/${prd.ProductRowId}`, prd, {
            'Content-Type' : 'application/json'
        });
        return response;
    }
    deleteData(id) {
        let response = axios.delete(`${this.url}/${id}`);
        return response;
    }
}


Listing 2: The HTTP Service class to make REST calls 


Our Single Page Application will use methods from the HTTP Service class to perform HTTP operations to Read and Write data. 


Step 3: 
In the src folder, add a new folder and name this folder as components. In this folder we will be adding React components. We will use React functional components. In the components folder add a new file and name it as ProductListComponent.jsx. In this file, add code as shown in listing 3 

import React, { useState, useEffect } from 'react';
import { HttpService } from '../services/httpservice';
import {Link} from 'react-router-dom';

const ProductListComponent=(props)=> {
    const [products, updateProducts] = useState([]);
    const service = new HttpService();
    useEffect(()=>{
        service.getData()
            .then(response=>{
                updateProducts(response.data);
            })
            .catch(error=>{
                console.log(`Error Occured ${error}`);
            }); 
         
    }, []);


    const remove=(id)=>{
        service.deleteData(id).then(response=>{
           window.location.reload();
        })
        .catch(error=>{
            console.log(`Error Occured ${error}`);
        }); 
    }

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

export default ProductListComponent;


Listing 3: The ProductListComponent 

The ProductListComponent, uses React Hooks like useState and useEffect. 

The useState Hook is used to define state property for the component and the method using which the state will be updated. The ProductListComponent uses the HttpService class instance to access its method to perform REST calls.

 The useEffect Hook is executed at the component level. This Hook invokes getData() method of the HttpService class. The useEffect will be executed for each rendering of the component. So to make sure that the Hook executes only once, we are passing an empty array as a dependency parameter to the Hook. This informs React that the Hook is not dependant on any state or props of the component. The data returned from the getData() method is updated in the products state property using updateProducts method. 

The ProductListComponent renders the HTML table based on the data from the products state property. The HTML table renders buttons to perform Edit and Delete operations. 

The Edit button contains the Route Link so that when the Edit button is clicked, the Route navigation will takes place to the Edit View based on the ProductRowId selected from the Edit button HTML table row. 

The Delete button is subscribed with onClick event and invokes remove() method of the component to delete the product based on the ProductRowId of the selected row. The remove() method invokes the deleteData() method of the HttpService class.


Step 4: 
In the components folder, add a new file. Name this file as CreateProductComponent.jsx. In this file we will add code for functional component to create new products. Add the code in this file as shown in Listing 4. 

import React, { useState } from 'react';
import {Link} from 'react-router-dom';
 
import {HttpService} from '../services/httpservice';

const CreateProductComponent=(props)=> {
    const service = new HttpService();
    const categories =['Electronics', 'Electrical', 'Food'];
    const manufacturers = ['MSIT', 'TSFOODS', 'LS-Electrical'];
    const [product, updateProduct] = useState({ProductRowId:0, ProductId:'', 
                                               ProductName: '', CategoryName:'',
                                               Manufacturer:'', Description:'', BasePrice:0});
     
    const clear=()=>(
        updateProduct({ProductRowId:0, ProductId:'', 
        ProductName: '', CategoryName:'',
        Manufacturer:'', Description:'', BasePrice:0})
    );

    const save=()=>{
        service.postData(product).then(response=>{
            updateProduct(response.data);
            props.history.push('/');
        })
        .catch(error=>{
            console.log(`Error Occured ${error}`);
        }); 
    }
  return (
    <div className="container">
    <h2>Create the New Product</h2>
                    <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)=>{updateProduct({...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)=>{updateProduct({...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)=>{updateProduct({...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)=>{
                            updateProduct({...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)=>{updateProduct({...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)=>{updateProduct({...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>
                   <hr/>
                   <Link to="/">Back to List</Link>   

</div>       
  );
}

export default CreateProductComponent;


Listing 4: Component to create new Product 

The CreateProductComponent is a functional component which is using React Hooks for defining state properties. The product is a state property declared using useState Hook. This Hook, declares the updateProduct method which will update product state property. 

The CreateProductComponent, contains constant arrays for categories and manufacturers. The clear() method updates the product state property to its default value. The save() method invokes postData() method of the HttpService class to post the product data so that a new product is created. 

The most important part of the CreateProductComponent is that this functional component accepts the props object. The props is an immutable object in React. currently we are using this props to define the routing navigation inside the save() method.

 If the new product is posted successfully, then the route navigation will load the default URL which we have defined in App.js as shown in Listing 1 above. The route navigation will load ListProductsComponents if the new product is created successfully. The CreateProductComponent  renders HTML input elements, Select elements and Buttons. 

Input and Select elements are bound to the properties from the product state property. These elements subscribes to the onChange event. This event is responsible to update properties of product state when value is entered in input elements or selected from options of select element. HTML select elements are populated based on categories and manufacturers array defined in the CreateProductComponent. HTML buttons are bound to clear() and save() method to clear HTML input elements and save new product respectively. 


Step 5: 
In the components folder, add a new file and name it as EditProductComponent.jsx. This component will be loaded when the Edit button is clicked from the HTML table row of the ProductListComponent as shown in listing 3. 

The Edit button executes the Router Link and sends the ProductRowId as a router parameter. The EditProductComponent accepts props object so that the router parameter value can be read by subscribing to the route. Most of the code of CreateProductComponent and EditProductComponent is same. 

The useEffect Hook is used to read the router parameter and fetch the product data by invoking the getDataById() method from the HttpService class. The save() method of the EditProductComponent invokes the putData() method of the HttpService class to update the product. The code for useEffect Hook and save() method of the EditProductComponent is shown in Listing 5. (Note: The code which is changed in EditProductComponent is only added here, rest of the code including the HTML markup is same as CreateProductComponent as shown in listing 4) 

 useEffect(()=> {
        const id = props.match.params.id;
        service.getDataById(id).then(response=> {
            updateProduct(response.data);
        }).catch(error=>{
            console.log(`Error Occured ${error}`);
        }); 
    },[]);
    
      const save=()=>{
        service.putData(product).then(response=>{
            updateProduct(response.data);
            props.history.push('/');
        })
        .catch(error=>{
            console.log(`Error Occured ${error}`);
        }); 
    }


Listing 5: The save() method and useEffectHook 


Step 6: 
Modify code of index.js to run the App component in the BrowserModule as shown in listing 6: 

import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <React.StrictMode>
  <BrowserRouter>
    <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);


The BrowserModule will load the router dom and will execute all components inside router dom To run the application, run the following command from the command prompt 

npm run start 


After successful execution, the command will open a browser with the http://localhost:3000 URL in the address bar. The ProductListComponent will be rendered as shown in Figure 1. Note that if there are no products in remote database then a No Record Found message will be displayed. 


Figure 1: The List of Products 


Since there are no products in the remote database, the No Record Found message is displayed. 
Click on Create Product link to display the Create Product Component. Enter Product information and click on the Save button. Figure 2 shows the Create Product Component with data entered in it. 


Figure 2: The Create Product Component 


Once we click on the Save button, the product will be created and the router will navigate to Product List as shown in Figure 3: 


Figure 3: The List of Products 

Likewise, click on the Edit button, this will route to the Edit view and based on the ProductRowId the record will displayed in Edit Product view. You can edit values and click on the Save button. The Product List will be displayed with updated product values. Similarly, on clicking of the Delete button, the record will be deleted. 

Conclusion:

React-Router-Dom is nice handy library using which we can develop Single-Page-Applications in React applications.






About The Author

Mahesh Sabnis is a Microsoft MVP having over 18 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). He also blogs regularly at DotNetCurry.com. Follow him on twitter @maheshdotnet

No comments: