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:
- The user needs to be able to toggle its state to open or close
- 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! 🦔
Top comments (0)