Creating a Collapsible Navigation Menu in React.js

Updated 8/15/2016

Today we're going to be making a collapsible navigation menu solution using nothing but React.js as an exercise in using event handlers, component state, and various React lifecycle methods. There is a much simpler and more elegant solution to achieve this exact same thing - my thanks go out to Mike for sharing his example in the comments below. But for the sake of practice and education, let's go ahead and dig in.

We'll be sticking mostly to the React portion here, but there was some SCSS work that I did to make the navigation pretty to look at, but I'll leave you to do most of that.


Getting Set Up

Really the only true dependency that we need here is React itself, but I am going to recommend (and use) react-icons material design icon font to give us access to navigation icon, as well as react-router to get your router connected. So install those dependencies:

$ npm install react-router

Next let's get these imported into a new file that we've named navigation.js inside the components directory of our app.

import React, { Component } from 'react';  
import { Link } from 'react-router';  

Get your navigation component created, then we're ready to get to the fun parts. Everything we'll be making below will go inside this component and above the render() function.

export default class NavContainer extends Component {

  //our other functions, and state will go here soon

  render() {
    <div className="nav_container">
      <div className="site_title"><Link to="/">WEBSITE TITLE</Link></div>
      //navigation will go here
    </div>
  }
}

Set our initial state and get window width

Let's look ahead a bit and see what we're going to need for this to work;

  1. We're going to need an if-then statement to see if it's time to render the mobile navigation menu or the desktop one.

  2. We'll need a way to tell if the mobile navigation is open or not.

So let's get our component's state declared with these:

  constructor(props) {
    super(props);
    this.state = {
      windowWidth: window.innerWidth,
      mobileNavVisible: false
    };
  }

After we've set the initial window width and told the component that the mobile navigation isn't visible right now, let's allow our state to update itself if the user changes the screen size.

  handleResize() {
    this.setState({windowWidth: window.innerWidth});
  }

  componentDidMount() {
    window.addEventListener('resize', this.handleResize.bind(this));
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize.bind(this));
  }

These three functions will set up an event listener once the component has mounted, and then it handles it to update our state with the new window width if it changes. This is going to allow us to test if it's time to draw the mobile navigation or the full one.

Full navigation menu

Next let's create a function that will return only the navigation menu. Creating a single function that handles the links to our menu will make changing them a breeze, and keeps us from repeating ourselves, since both the full and mobile menus will feature the same links.

  navigationLinks() {
    return [
      <ul>
        <li key={1}><Link to="about">ABOUT</Link></li>
        <li key={2}><Link to="blog">BLOG</Link></li>
        <li key={3}><Link to="portfolio">PORTFOLIO</Link></li>
      </ul>
    ];
  }

Mobile navigation menu

Make another function that asks if the mobile menu is visible or not, and display these links based on the answer, then create a click handler for when the mobile menu button is clicked (or touched):

  renderMobileNav() {
    if(this.state.mobileNavVisible) {
      return this.navigationLinks();
    }
  }

  handleNavClick() {
    if(!this.state.mobileNavVisible) {
      this.setState({mobileNavVisible: true});
    } else {
      this.setState({mobileNavVisible: false});
    }
  }

Tying the collapsible menu together

Lastly, we're going to put it all together with this function that will render either the mobile navigation or the full one:

  renderNavigation() {
    if(this.state.windowWidth <= 1080) {
      return [
        <div className="mobile_nav">
          <p onClick={this.handleNavClick.bind(this)}><i class="material-icons">view_headline</i></p>
          {this.renderMobileNav()}
        </div>
      ];
    } else {
      return [
        <div key={7} className="nav_menu">
          {this.navigationLinks()}
        </div>
      ];
    }
  }

We're simply running an if then statement that checks if it exceeds the width that we've defined, and returning either a div containing the mobile navigation with the material icon that will pass click events to our handler, or the full navigation.

Notice that each of these menus are nested inside a div with a name. Thanks to SCSS we can style the same ul of items with two completely different looks to fit the needs of our mobile and desktop navigation.

You'll now only to need to call this function inside the render() function of our component and you're done!

  render() {
    return(
      <div className="nav_container">
        <div className="site_title"><Link to="/">WEBSITE TITLE</Link></div>
        {this.renderNavigation()}
      </div>
    )
  }

Hopefully this exercise has helped you to learn some things about event listeners and React lifecycle methods. Like I said above, this is not the most efficient way to accomplish a collapsible navigation menu, but could be useful in some cases as a point of reference. Since completing this tutorial back in May I've seen lots of awesome solutions that you should check out. One of the commenters below left a great one that I thoroughly enjoy and I recommend checking it out if you're looking for a production ready solution to your applications navigation needs.

Thanks for reading, and sharing, and please leave any questions or comments you may have below. Until next time, happy coding!

You can take a look at the complete source code for this project here.

.

David Meents

React.js developer, web designer, and business owner.

Subscribe to David Meents

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!
comments powered by Disqus