DEV Community

Arnelle Balane
Arnelle Balane

Posted on • Originally published at blog.arnellebalane.com on

Building a site navigation menu using React Hooks

I’m currently learning React, and since I learn better by building stuff, I decided to rebuild my personal website with it. It’s still a work in progress but there is one component which I found interesting to build: the site’s navigation menu.

It’s just a simple menu, and I only have two requirements for it:

  1. The user needs to be able to toggle its state to open or close
  2. It should close when the user navigates to a different page

Initial setup

I initially built a static version of the site, composed of the top-level App component, a Header component, and a Menu component. The App component looks like this:

// App.jsx

import Header from './Header.jsx';

function App(props) {
    const isMenuOpen = false;

    return (
        <div>
            <Header isMenuOpen={isMenuOpen} />

            {/\* Other stuff \*/}
        </div>
    );
}

As shown in the code snippet, the App component has an isMenuOpen variable which it passes to Header as the isMenuOpen prop. The Header in turn passes the same isMenuOpen prop to Menu. The value of this variable controls whether Menu should be shown or hidden.

isMenuOpen component state

Initially, isMenuOpen is just a variable whose value I manually change to update the UI. This is okay for the initial static version of the app, but I don’t really want that on the actual app. I want the component to keep track of this variable, modify its value in response to a user action (e.g. a click on the toggle menu button), and re-render the UI based on its new value.

To achieve that, I need to make isMenuOpen an actual state on the App component. Normally this would be done by converting App from a functional component into a class component. This is because functional components can’t have state while class components can. If I follow this approach, the App component will become:

// App.jsx

class App extends React.Components {
    constructor(props) {
        super(props);
        this.state = {
            isMenuOpen: false
        };
        this.handleToggleMenu = this.handleToggleMenu.bind(this);
    }

    handleToggleMenu() {
        this.setState(state => ({
            isMenuOpen: !state.isMenuOpen
        }));
    }

    render() {
        return (
            <div>
                <Header 
                    isMenuOpen={this.state.isMenuOpen} 
                    onToggleMenu={this.handleToggleMenu}
                />

                {/\* Other stuff \*/}
            </div>
        );
    }
}

I would have done it this way, but then it just happened that I just recently read about React Hooks from the docs.

React Hooks gives us access to features such as states and lifecycle methods without having to use class components (in fact, they should only be used in functional components). It seemed like I had an opportunity to use React Hooks for my navigation menu so I decided to try it out.

Make sure to use the right React version

At the time of writing, React Hooks is still on preview, and is only available in React v16.8.0-alpha.0. I had to update the corresponding packages to use the right versions:

npm install react@16.8.0-alpha.0 react-dom@16.8.0-alpha.0

Using the useState hook

With the correct versions of react and react-dom installed, I can now start using React Hooks. Since I want to use states in my functional App component, I used React’s built-in useState hook.

import {useState} from react;

Then used it to initialize the isMenuOpen state:

function App(props) {
    const [isMenuOpen, setIsMenuOpen] = useState(false);
}

The useState hook accepts one argument which is the initial value to set the state to, and returns an array of two things: the current state value and a function used to update the state value.

And just like that, I now have a reactive isMenuOpen state with just very minimal changes in the code. I was able to use state in my component while keeping it as a functional component. In fact, to me it still kinda looks like I’m just declaring the isMenuOpen variable from the static version of the component. The complete App component now looks like:

// App.jsx

function App(props) {
    const [isMenuOpen, setIsMenuOpen] = useState(false);

    return (
        <div className={style.wrapper}>
            <Header
                isMenuOpen={isMenuOpen}
                onToggleMenu={() => setIsMenuOpen(!isMenuOpen)}
            />

            {/\* Other stuff \*/}
        </div>
    );
}

Detecting page navigations

At this point the navigation menu already opens and closes when clicking on the menu button inside the Header component. The next thing that I needed to do was to make sure to close it when a menu item gets clicked. Otherwise, the menu will continue covering the page even after navigating to the next page.

I am using React Router to route URLs to specific page components. To detect page navigations, I first needed access to the history object being used by React Router from the App component. This was achieved by wrapping App inside the withRouter higher-order component, which passed history as one of App’s props.

// App.jsx

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

function App(props) {
    const history = props.history;

    // Other stuff
}

export default withRouter(App);

The history object has a .listen() method which accepts a callback function that it will call every time the current location changes. Subscribing to these changes is usually done in the component’s componentDidMount lifecycle method (and unsubscribing in componentWillUnmount), which requires a class component and will make App look like this:

// App.jsx

class App extends React.Component {
    // constructor(props)
    // handleToggleMenu()

    componentDidMount() {
        this.unlisten = this.props.history.listen(() => {
            this.setState({
                isMenuOpen: false
            });
        });
    }

    componentWillUnmount() {
        this.unlisten();
    }

    // render()
}

But again, I didn’t want to convert my App component into a class component. And also I just read that there is a built-in React Hook for doing exactly this pattern, so I decided to use it instead.

Using the useEffect hook

The pattern of registering something in a component’s componentDidMount and unregistering it in componentWillUnmount is apparently very common that it got abstracted into its own React Hook, the useEffect hook.

import {useEffect} from 'react';

The useEffect hook accepts a function containing code that will normally run inside the componentDidMount (and componentDidUpdate) lifecycle method; in my case, that would be code to listen to changes to the current history location and closing the menu when it does.

// App.jsx

function App(props) {
    useEffect(() => {
        props.history.listen(() => {
            setIsMenuOpen(false);
        });
    });

    // Other stuff
}

We can also return a function containing code that will normally run inside the componentWillUnmount lifecycle method; in my case, stop listening for changes to the current history location. Calling history.listen() already returns such function so I can just return it right away.

// App.jsx

function App(props) {
    useEffect(() => {
        return props.history.listen(() => {
            setIsMenuOpen(false);
        });
    });

    // Other stuff
}

And these are all the changes needed to make the App component close the navigation menu on page navigations. No need to convert it to a class component and setup lifecycle methods. All the related code are located in close proximity to each other instead of being separated in different places in the component code.

Final App component

After applying all these changes, the App component, complete with the stateful navigation menu which closes on page navigation, now looks like this:

// App.jsx

import {useState, useEffect} from 'react';
import {withRouter} from 'react-router-dom';
import Header from './Header.jsx';

function App(props) {
    const [isMenuOpen, setIsMenuOpen] = useState(false);

    useEffect(() => props.history.listen(() => {
        setIsMenuOpen(false);
    }));

    return (
        <div>
            <Header
                isMenuOpen={isMenuOpen}
                onToggleMenu={() => setIsMenuOpen(!isMenuOpen)}
            />

            {/\* Other stuff \*/}
        </div>
    );
}

export default withRouter(App);

I could have gone even further by making a generic React Hook for such functionality, in case I need to use it again somewhere else. We can use these built-in React Hooks to build more hooks. But I guess I’ll just reserve that for another day when I actually need to.

Summary

In this article I walked through how I made my site’s navigation menu using React Hooks. We used the built-in useState hook to keep track of the menu’s open/close state, and the built-in useEffect hook to listen to changes in the current location (and cleanup after when the component is going to be removed). After applying the changes, we end up with a functional component that has its own state.

This is the first time that I’ve used React Hooks on something and so far I totally enjoyed the experience. I think the code is more readable and easier to figure out compared to using class components with lots of lifecycle methods, since I didn’t need to look in multiple separate places to understand a component’s functionality. Instead, all the related functionality are defined in one place. Also, we are able to build custom, more complex hooks out of the built-in ones if we want to, and reuse these functionalities all over our application. I’m really looking forward to using React Hooks more in the future.

Resources

Thanks for reading this article! Feel free to leave your comments and let me know what you think. I also write other articles and make demos about cool Web stuff. You can check them out on my blog and on my GitHub profile. Have a great day! 🦔


Oldest comments (0)