ReactJS multi-level navigation menu with MaterialUI and TypeScript
In this article I will share my experience about how I was developing the sidebar navigation menu of my hobby open-source project Material Admin React with React + TypeScript + MaterialUI .
The source codes are available in this GitHub repository
Stage 1 Getting started and main setup
Make sure you have NodeJS installed along with TypeScript version 3.5 or above. Also for the code editing I’m using VSCode.
Scaffold a blank ReactJS + TypeScript Project with CreateReactApp
npx create-react-app material-navigation --typescript
Change to the newly generated material-navigation directory, install a few dependencies
npm install --save @material-ui/core @material-ui/icons react-router-dom @types/react-router-dom
and add the Roboto font for MaterialUI by adding this tag into public/index.html file <head> section
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
Running the
npm start
command should compile the project and launch a dev server without any errors.
Now let’s clean-up the project, remove the boilerplate code and scaffold a base layout
We’re using functional components with React Hooks. In this case useStyles is an internal MaterialUI hook, which provides us an object encapsulated classnames defined below read more about MaterialUI styles. The clsx is a small util library for composing string class names. We’re also annotating the components with React.FC TypeScript type which is a shorthand for FunctionComponent type.
This should render a blank page with a drawer. Nothing spectacular yet, but we’re just starting! :)
Stage 2: Implementing the menu layout
Tl;DR: Stage 2 source codes | CodeSandbox
Now let’s implement the basic layout of our menu. We’ll be using MaterialUI Lists component. Create a new AppMenu.tsx file with the following content, and import it into the App.tsx file into the <Drawer> component
This should render as the image below
So this is already more interesting, but as you can see this implementation has some issues:
- There’s too much code duplication, also what if we want to adjust the layout of all menu items, or add classes? Then we have to re-write all the elements
- We need to create collapse component and state for each nested menu groups
- The menu links can even come from API, so the menu should be fine with displaying that
What if we’ll just defined a menu item component, which could have it’s own submenu, and the submenu items might also have their submenus, etc. So we would have this kind of dynamic tree structure
menu link
menu link
submenu link
sub-submenu link
submenu link
...etc
Stage 3 Implementing recursive menu item
Tl;DR: Stage 3 source codes | CodeSandbox
Our requirements for the menu link component would be:
- Each menu item has a name
- Each menu item might have a link (we’ll be setting up the react-router in the next stage), icon and nested menu items, which could be expanded by clicking on the parent
- If the menu item has children, we should show up/down arrows nearby the menu name
Let’s create a new blank AppMenuItem.tsx component and write-down the component React propTypes based on the the requirements
Here we have two main types of component properties definitions: React PropTypes (AppMenuItemPropType) and TypeScript compile-time properties (AppMeniItemProps). Because we don’t want to duplicate our properties definition one more time in TypeScript, there’s a cool way by inferring TypeScript types by defined runtime PropTypes. Sometimes it should be still adjusted though. In our case we still need to define more advanced types for recursive items call
Now let’s add the code implementation
So the component returns a React Fragment, which always contains the MenuItemRoot and might have MenuItemChildren. Also it checks if there is a menu icon and an expand icon. MenuItemChildren is wrapped by Collapse MaterialUI component which toggles in/out depending on the component open state. Also the MenuItemChildren component recursively calls AppMenuItem component itself.
Now let’s remove the hard-coded content from the AppMenu.tsx component and render the menu list by a JS loop.
Although It’s exactly the same layout like in the previous stage, its more DRYer now :)
Stage 4 Adding react-router support
Tl;DR: Stage 4 source codes | CodeSandbox
Ok, so this looks good, now we have this infinite-level menu with nice-looking buttons, but they aren’t very usable yet without the navigation links. The common use case would be using react-router.
From this link we can learn that the ListItem component of MaterialUI can render a custom component, so we need to pass React-Router NavLink to our ListItem, when the AppMenuItem has link property set.
At first glance it might seem that we can just pass <NavLink /> component as property to <ListItem/>, but TypeScript deosn’t think so
This is a fancy TS error, which is super-hard to debug, but after debugging it for a while I’ve got to conclusion that there is a NavLink and ListItem components properties incompatibility. To solve that incompatibility we need some intermediated component, which will pass the props to the link component in a correct way. From this MaterialUI guide we also know that a) the intermediate component should be defined outside of AppMenuItem to not redraw on each function call and b) should support a reference (functional components in React don’t support ref), so we’ll need to use React’s forwardRef method.
Also in our case the link is an optional property, so we need to use React-Router NavLink component in ListItem , only if the link is defined. That brings us to the following pseudo-code
AppMenuItemRoot =>
// Return regular <ListItem /> if the link is missing
// Return <ListItem /> with <NavLink /> as component if link is set
Let’s create a new component AppMenuItemComponent.tsx. The implementation will look like this
We’ve used react’s forwardRef method, because the ListItem’s component should support a ref property. See Material-UI Composition page for more info
Now we can import the AppMenuItemComponent and use it inside of AppMenuItem, replacing the ListItem. Now let’s setup the React-Router in App.tsx file, and define a few routes matching our links and add the active link style
The final result and source codes are here
Congratulations, we‘ve made it! 🎊 🎉 😊
Advanced features
You may see more advanced implementation of the menu which supports collapsed state with tooltips in Material Admin React project.
Let me know if you liked this article, and feel free to leave your remarks in comments!
Follow me on Medium | Twitter | GitHub to be notified about the new stuff I’m making!