React: how to switch between site navbar menu without reloading the app

Having a navbar menu on a web application is very common. Traditionally, the user has to load the target page associated with a navbar menu by clicking on a button or anchor. As the result, the user sends a new URL request to the server and the server returns the rendered webpage to the user. This is a costly approach since each time there will be a response time and also the server needs to handle many requests at a time just for loading new pages. 

The other approach for navigating through a web application is to handle all page switches on the client side with help of Javascript. In this approach, the user does not need to wait for the new page on each page switch, and also the server gets relieved from answering page switch requests. In this writing, we see how to apply this approach with React

Before we start, here are the React version and needed libraries for this article:

"dependencies": {
    "@testing-library/jest-dom": "^5.16.3",
    "@testing-library/react": "^12.0.0",
    "@testing-library/user-event": "^12.0.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^5.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4",
    "query-string": "^7.1.1"
  },

**Note**: Some decisions such as the code structure, function name, etc are my personal subjective choices. I am not claiming they are the best and you do not have to use the same. 

Scenario 

Let’s define a simple scenario. Let’s say we have two pages page1 and page2. Each page has a sentence like “This is page X”. We like to implement a navigation menu that allows users to switch among them without sending a new request to the server (without reloading the app).

Implementation

First, we define our class component in React named Home. The component is defined inside a directory named components so our new Home component path is like: “/src/components/Home.jsx”

Then, we implement our navbar with Help of ul/li and the Link component from the react-router-dom library. Also, we define a new URL path for our navbar links with the help of the “to” property. We also add our page content. 

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

class Home extends React.Component{
    render(){
        return(
            <div>
                <ul className='menu'>
                    <li className='menu-item' key={"page1"}>
                        <Link to="/page1" className='menu-anchor' value="page1">Page1</Link>
                    </li>
                    <li className='menu-item' key={"page2"}>
                        <Link to="/page2" className='menu-anchor' value="page2">Page2</Link>
                    </li>                    
                </ul>          
            </div>            
        );
    }

}

export default Home;

Note: Do not use the HTML anchor <a> instead of the Link component since the browser sends a new request to the server. 

There is an issue with the above code. The page contents should not be visible on the render. They need to get shown only when a user clicks on the related navbar link. To hide both contents, we define two state variables named page1Flag and page2Flag and we set both of them to false. Then, we and (&&) them with the page contents to hide them. 

class Home extends React.Component{
    constructor(props){
        super(props);
        this.state = ({
            page1Flag: false,
            page2Flag: false
        });       
    }
render(){
        return(
            <div>
                <ul className='menu'>
                    <li className='menu-item' key={"page1"}>
                        <Link to="/page1" className='menu-anchor' value="page1">Page1</Link>
                    </li>
                    <li className='menu-item' key={"page2"}>
                        <Link to="/page2" className='menu-anchor' value="page2">Page2</Link>
                    </li>                    
                </ul>
                {this.state.page1Flag && <div>This is Page 1</div>}
                {this.state.page2Flag && <div>This is Page 2</div>}
            </div>            
        );
    }

The next issue is with the URL path for /page1 and /page2. The issue is we did not define these routing paths for our application. To do this, we use the react-router-dom library in the App.js 

import { BrowserRouter, Route, Switch} from 'react-router-dom';
import Home from './components/Home';


function App() {
  return (
    <div className="App">      
      <BrowserRouter>
      <Switch>              
          <Route exact path="/:tab?" component={Home} />         
      </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

The most important part of the above code is the /:tab? In the Route. This tells React that our Home component accepts extra optional URL paths. The optional part is indicated by the question mark (?). We use the name tab later to get the URL path from the request.

Now that we have everything, we need to tell react what to do when a user clicks on the navbar link (we want to show the page content). To do this, we define a click handler for our links. The handler checks the target clicked the link value property to check the target page. Depending on the value, the click handler function set true/false for the boolean state variables associated with each page content. We also pass the handler function to the onClick event handler for our links. 

 navbarClickHandler(e){
        let target = e.target.getAttribute("value");        
        if(target === "page1"){
            this.setState({
                page1Flag: true,
                page2Flag: false                
            });
        }
        else if(target === "page2"){
            this.setState({
                page1Flag: false,
                page2Flag: true 
            });
        }
    }
<ul className='menu'>
       <li className='menu-item' key={"page1"}>
           <Link to="/page1" className='menu-anchor' onClick={this.navbarClickHandler} value="page1">Page1</Link>
       </li>
       <li className='menu-item' key={"page2"}>
           <Link to="/page2" className='menu-anchor' onClick={this.navbarClickHandler} value="page2">Page2</Link>
       </li>                    
</ul>

We also need to bind our click handler function in the component constructor. 

 this.navbarClickHandler = this.navbarClickHandler.bind(this);

The code is almost finished. However, there is still one issue with the code. The issue is when a user directly wants to jump to one of the pages (by entering the URL), he/she cannot do it. The reason is that our URL processing happens in the onClick handler. Therefore, opening for instance “http://www.MY_SITE/page1” does not work. 

To make it work we need to define a URL processor that runs in the componentDidMount step of our component life cycle. The function body is exactly like the click handler with one important difference. Here we get the target page name from the URL with help of the tab placeholder that we defined in the App.js

processUrl(){
        let tab = this.props.match.params.tab;
        if(tab === "page1"){
            this.setState({
                page1Flag: true,
                page2Flag: false                
            });
        }
        else if(tab === "page2"){
            this.setState({
                page1Flag: false,
                page2Flag: true 
            });
        }
    }
componentDidMount(){
        this.processUrl();
    }

Here also we need to bind our new function in the component constructor. 

this.processUrl = this.processUrl.bind(this);

Note: To use the URL reading via match.params you need to export the component with withRouter in React.

Here is the complete code for Home.jsx:

import React from 'react';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom'; 


class Home extends React.Component{
    constructor(props){
        super(props);
        this.state = ({
            page1Flag: false,
            page2Flag: false
        });
        this.navbarClickHandler = this.navbarClickHandler.bind(this);
        this.processUrl = this.processUrl.bind(this);
    }

    processUrl(){
        let tab = this.props.match.params.tab;
        if(tab === "page1"){
            this.setState({
                page1Flag: true,
                page2Flag: false                
            });
        }
        else if(tab === "page2"){
            this.setState({
                page1Flag: false,
                page2Flag: true 
            });
        }
    }

    navbarClickHandler(e){
        let target = e.target.getAttribute("value");        
        if(target === "page1"){
            this.setState({
                page1Flag: true,
                page2Flag: false                
            });
        }
        else if(target === "page2"){
            this.setState({
                page1Flag: false,
                page2Flag: true 
            });
        }
    }

    componentDidMount(){
        this.processUrl();
    }

    render(){
        return(
            <div>
                <ul className='menu'>
                    <li className='menu-item' key={"page1"}>
                        <Link to="/page1" className='menu-anchor' onClick={this.navbarClickHandler} value="page1">Page1</Link>
                    </li>
                    <li className='menu-item' key={"page2"}>
                        <Link to="/page2" className='menu-anchor' onClick={this.navbarClickHandler} value="page2">Page2</Link>
                    </li>                    
                </ul>
                {this.state.page1Flag && <div>This is Page 1</div>}
                {this.state.page2Flag && <div>This is Page 2</div>}
            </div>            
        );
    }

}

export default withRouter(Home);

The End.