Interactive Navigation in React SPA
Tags: react, javascript, webdev
Published: 2020-12-21 21:31:34

I wanted a way to show a top navigation bar in the hero section and a hamburger drop-down navigation as a user scrolls down the rest of the page as part of my React single page application. 

After a lot of Googling along with some trial and error, I put together a few ideas to achieve my desired end result which looks like this: 

![](https://i.imgur.com/1OGO1wX.gif)

### Getting Started 
I used: 
- [react-scroll](https://www.npmjs.com/package/react-scroll) `npm i react-scroll`
- [react-burger-menu](https://www.npmjs.com/package/react-burger-menu) `npm i react-burger-menu`
- Two separate components: one for top nav and one for the drop-down nav

### Setting Up Burger Menu 
[Instructions for burger set up and customization are here](https://www.npmjs.com/package/react-burger-menu). Here is what my Sidenav.js and Sidenav.css files looked like after tweaking the default:

```
import React, {Component} from 'react';
import './Sidenav.css';
import { slide as Menu } from 'react-burger-menu';
import {Link} from 'react-scroll';

class Sidenav extends Component {

  render () {

    return (
      <div className='sidenav'>
        <Menu width={ '15%' } >

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="home" spy={true} smooth= 
           {true}>Home</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="about" spy={true} smooth= 
           {true}>About</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="work" spy={true} smooth= 
           {true}>Work</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="contact" spy={true} smooth= 
           {true}>Contact</Link>

          <a className='nav-li' target='_blank' rel="noopener 
           noreferrer" href='linktoyourblog'>Blog</a>

        </Menu>    
      </div>
    );
  }
}

export default Sidenav;
```
```
/* Position and sizing of burger button */
.bm-burger-button {
  position: fixed;
  width: 30px;
  height: 25px;
  left: 10px;
  top: 36px;
}

/* Color/shape of burger icon bars */
.bm-burger-bars {
  background-color: #EFC6BA;
}


/* Position and sizing of clickable cross button */
.bm-cross-button {
  height: 24px;
  width: 24px;
}

/* Color/shape of close button cross */
.bm-cross {
  background: #222222;
}

/*
Sidebar wrapper styles
Note: Beware of modifying this element as it can break the animations - you should not need to touch it in most cases
*/
.bm-menu-wrap {
  position: fixed;
  height: 100%;
}

/* General sidebar styles */
.bm-menu {
  background: #FFFFFF;
  padding: 2.5em 0.5em 0;
  font-size: 1.15em;
}

/* Morph shape necessary with bubble or elastic */
.bm-morph-shape {
  fill: #FFFFFF;
}

/* Wrapper for item list */
.bm-item-list {
  color: #FFFFFF;
  padding: 0.8em;
}

/* Individual item */
.bm-item {
  display: inline-block;
}

/* Styling of overlay */
.bm-overlay {
  background: #FFFFFF;
}

/* Styling of links */
.nav-li {
  font-family: 'Playfair Display';
  color: #000000;
  outline: none;
  padding: 10%;
  text-decoration: underline;
  text-transform: uppercase;
}

/* Removes purple outline when clicked */
.nav-li:focus {
  outline: none;
}

.nav-li:hover {
  text-decoration: none;
  font-weight: bold;
}
```
#### Managing State
At this point, the burger menu is working. There are additional state and event handlers available which are useful. By adding the following above `render()`, the side menu will close when a link is clicked rather than having to click on the 'X':

```

    state = {
      menuOpen: false,
    }

    handleStateChange (state) {
      this.setState({menuOpen: state.isOpen})  
    }
    
    // closes menu on link click
    closeMenu () {
      this.setState({menuOpen: false})
    }
```

Next, I added props to the Menu and Link components in `return()`. 
- Menu receives `isOpen={this.state.menuOpen} 
  onStateChange={(state) => this.handleStateChange(state)}`
- Link (and `<a>`) receives `onClick={() => this.closeMenu()}`

```
<Menu width={ '15%' } isOpen={this.state.menuOpen} 
  onStateChange={(state) => this.handleStateChange(state)}>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="home" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Home</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="about" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>About</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="work" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Work</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="contact" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Contact</Link>

 <a className='nav-li' target='_blank' rel="noopener 
  noreferrer" href='linktoyourblog' onClick={() => 
  this.closeMenu()}>Blog</a>

</Menu>  
```

Alright! The burger menu is fully functional now. 

So... how do we only make it visible as a user scrolls away from the landing view?

###componentDidMount() and onscroll events

[componendDidMount()](https://reactjs.org/docs/react-component.html#componentdidmount) is invoked immediately after a component is mounted. As this happens we will watch for a scroll event to initiate a state change which changes the `<div>` opacity. 

In Sidenav.js:

```
 state = {
      menuOpen: false,
      opacity: '0'
    }

    // will show sidenav when scroll position is above 500
    componentDidMount() {
      if (typeof window !== 'undefined') {
        window.onscroll = () => {
          let currentScrollPos = window.pageYOffset;
          let maxScroll = document.body.scrollHeight - 
          window.innerHeight;
          if (currentScrollPos < 500 && currentScrollPos < 
           maxScroll) { 
            this.setState({ opacity: '0'})
            // to get position: console.log(currentScrollPos)
           } else {
            this.setState({ opacity: '1' })
          }
        }
      }
    }
```

What's happening here? I'm using state and inline CSS styling to control the visibility of the drop-down menu. I added opacity to our state and give it a default value of 0 so that it does not display on page load. In the function, I use `setState` to change the opacity based on the current scroll position of the page. 

Let's go back to `return()`. All of the content should be wrapped in a `<div className='sidenav>`, to which we add inline styling: 

```
<div className='sidenav' style={{ opacity: `${this.state.opacity}`}}>

...

</div>
```

That's it! Now the drop-down is only visible after a certain point on the page view. 

###Conclusion & Addressing Performance
A somewhat hodgepodge interactive navigation design for SPA's. 

After doing more research on `componentDidMount()` and `onscroll` events, I noticed some concerns about performance issues using this type of approach. In its current usage, this isn't *completely* killing my app's performance.

Upon running a Lighthouse audit I get: 
![](https://i.imgur.com/axtsYrF.png)
Although, that is a drop from the previous 97 on Performance and 100 on Best Practices. :eyes:

That being said, I'd love to have a discussion on performance or perhaps a more efficient way to implement this interactive navigation that I'm not aware of-- so if you have something to teach me, drop a comment below! :thought_balloon: