Create a Dynamic Sidebar Menu in React (with Multi-level Nesting)
The Task
Recently, I wanted to create a sidebar menu with multiple items/children nesting i.e a multilevel drop-down navigation menu but with a catch: it wasn’t known how many items (and their children), i.e the menu options, were going to be there and also new menu items could be added during or even after the development. So the challenge that lied ahead was not only to actually have a nested menu (something which I was doing for the first time) but also to have a code which was dynamic and of course as short and efficient as possible.
For the above problem, I used Material-UI which comes as a gift with React. Material-UI is a UI framework for React that provides various components implementing Google’s Material Design guidelines. If you haven’t had the opportunity to use it until now, take a look here at the official docs.
An Example
If you’d like to get a better picture of what exactly we’ll be building have a look at the sidebar menu of Material-UI’s official website which has a ‘nested’ menu. As you can see there are multiple options (items) like “Component Demos”, “Component APIs”, “Getting Started” etc. and then when you click on one of them, either they expand downwards showing the children items or they navigate us to a specific location.
As an example, if you select, “Component Demos”, a list of all the Components falls below with the children, like Drawer, App Bar etc. but when one of them is selected, the browser navigates to their respective demo pages. This is a single level (parent - child) nesting. With this example, we’ll be making our project capable of having multi-level nesting.
Now that we have all the basic information out there, let’s roll!
The Dependencies
We’ll build this example task with Facebook’s very own create-react-app.
$ npx create-react-app nested-menu && cd nested-menu
To learn about create-react-app, you can check out the official git repo here.
Also, if you’d like to know more about npx, this is a great article.
Now, we’ll add the Material-UI package to our project:
$ npm i @material-ui/core
Since a menu-bar is a navigation component, we’ll be navigating to desired locations through the last nested child (leaf node). For this, we’ll be using the <Link>
tag of react-router-dom
. To add the dependency:
$ npm i react-router-dom
Routing in React can be done using its standard routing library React Router and react-router-dom is one of the three packages that complete it. To know more about react-router-dom and in general React router, here is a great article.
Also, apart from the components of Material-UI, we’ll be using Material-UI icons as well from the same package.
The Code
First, we’ll create a JSON file with our menu options’ name as well as the URL to which the item must navigate to enabling us to have a very generic code for navigation as well as item naming.
menuItems.json
{
"data" : [
{
"name": "Item1",
"url": "/item1"
},
{
"name": "Item2",
"url": "/item2"
},
{
"name": "Item3",
"children": [
{
"name": "Child31",
"url": "/child31"
},
{
"name": "Child32",
"url": "/child32"
},
{
"name": "Child32",
"url": "/child32"
}
]
},
{
"name": "Item4",
"children": [
{
"name": "Child41",
"url": "/child41"
},
{
"name": "Child42",
"url": "/child42"
},
{
"name": "Child43",
"children": [
{
"name": "Child431",
"url": "/child431"
},
{
"name": "Child432",
"url": "/child432,"
},
{
"name": "Child433",
"url": "/child433"
}
]
}
]
}
]
}
Inside the src/
folder create a file named as MenuBar.jsx
with the below code.
For creating the menu we’ll be using the Drawer component of Material-UI which basically is a standard navigation component that can be put permanently or temporarily on the screen.
MenuBar.jsx
import React, { Component } from 'react'
import List from 'material-ui/core/List'
import ListItem from 'material-ui/core/ListItem'
import ListItemText from 'material-ui/core/ListItemText'
import Collapse from 'material-ui/core/Collapse'
import ExpandLess from 'material-ui/icons/ExpandLess'
import ExpandMore from 'material-ui/icons/ExpandMore'
import Drawer from 'material-ui/core/Drawer'
import { withStyles } from 'material-ui/core/styles'
import { Link } from 'react-router-dom'
import menuItems from './menuItems'const styles = {
list: {
width: 250,
},
links: {
textDecoration:'none',
}
menuHeader: {
paddingLeft: '30px'
}};class MenuBar extends Component {
constructor( props ) {
super( props )
this.state = {}
}// this method sets the current state of a menu item i.e whether it is in expanded or collapsed or a collapsed statehandleClick( item ) {
this.setState( prevState => (
{ [ item ]: !prevState[ item ] }
) )
}// if the menu item doesn't have any child, this method simply returns a clickable menu item that redirects to any location and if there is no child this method uses recursion to go until the last level of children and then returns the item by the first condition.handler( children ) {
const { classes } = this.props
const { state } = thisreturn children.map( ( subOption ) => {
if ( !subOption.children ) {
return (
<div key={ subOption.name }>
<ListItem
button
key={ subOption.name }>
<Link
to={ subOption.url }
className={ classes.links }>
<ListItemText
inset
primary={ subOption.name }
/>
</Link>
</ListItem>
</div>
)
}
return (
<div key={ subOption.name }>
<ListItem
button
onClick={ () => this.handleClick( subOption.name ) }>
<ListItemText
inset
primary={ subOption.name } />
{ state[ subOption.name ] ?
<ExpandLess /> :
<ExpandMore />
}
</ListItem>
<Collapse
in={ state[ subOption.name ] }
timeout="auto"
unmountOnExit
>
{ this.handler( subOption.children ) }
</Collapse>
</div>
)
} )
}render() {
const { classes, drawerOpen, menuOptions } = this.props
return (
<div className={classes.list}>
<Drawer
variant="persistent"
anchor="left"
open
classes={ { paper: classes.list } }>
<div>
<List>
<ListItem
key="menuHeading"
divider
disableGutters
>
<ListItemText
className={ classes.menuHeader }
inset
primary="Nested Menu"
/>
</ListItem>
{ this.handler( menuItems.data ) }
</List>
</div>
</Drawer>
</div>
)
}
}export default withStyles(styles)(MenuBar)
In the above code snippet, as you can see we have put our menu options inside a list that gets set by the handler method in the Drawer component.
This method returns the menu items as list items. The method performs one of two actions depending upon whether the given menu item has any child or not. If the menu item doesn’t have any child, the method directly returns a list item which simply acts as a navigation button through the <Link>
tag as seen in the first return statement of the if condition.
But when the menu item does have a sub-menu item(s), the second return statement is executed and this is where the crux of our solution lies. The menu item i.e the ListItem
here acts as a button which is used to collapse/expand the sub-menu item(s). The onClick
handler of this ListItem
is the handleClick
method that performs the NOT operation on the current item’s state. Also, the ListItemText
i.e the item’s name here is preceded by ExpandLess
and ExpandMore
icon depending on the current state of the menu item representing whether it is collapsed or expanded.
The children of the menu item are then set using recursion wherein the items are present in the Collapse component of Material-UI which is basically a vertical drop-down and are set via the handler method. This way of setting the children using recursion makes our menu component dynamic and capable of having as many children as possible.
Thus by having:
- URLs and item names in a JSON file
- recursive handler method
- item’s name based state setting in the
handleClick
method
our menu component becomes capable of handling any number of children and navigating to the required location with very generic code.
We’ll simply import our menu file inside App.js
.
App.js
import React, { Component } from 'react';
import MenuBar from './MenuBar'class App extends Component {
render() {
return (
<MenuBar/>
)
}
}export default App;
And our App.js
in the index.js
file such that BrowserRouter
encapsulates our application.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter } from 'react-router-dom'ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>
),
document.getElementById('root'));
After running it should render in the browser, a sidebar menu, like the image below:
This is the most basic form of a nested menu structure and with the least possible styling. You can add much more styling and some more functionalities.
That’s it, folks. This is where my first Medium article ends and I take a bow. If you have any questions and suggestions, feel free to leave comments.
Thanks for reading. Ciao!!