ReactJS multi-level navigation menu with MaterialUI and TypeScript

Gevorg Harutyunyan
5 min readAug 2, 2019

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

Tl;DR: Stage 1 sources are available here | CodeSandbox

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

Base layout setup

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! :)

The base layout

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

AppMenu.tsx 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

AppMenuItem.tsx component propTypes ant types interface

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

AppMenuItem.tsx component 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

Type ‘{ children: any[]; button: true; className: string; onClick: () => void; component: typeof NavLink; to: string | null; }’ is not assignable to type ‘IntrinsicAttributes & { action?: ((instance: ButtonBaseActions | null) => …

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

ReactJS multi-level navigation menu with MaterialUI & TypeScript

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!

--

--

Gevorg Harutyunyan

Co-Founder at JS Conf Aremenia and React Conf Armenia, Front-End Architect at Screenful, Founder at ModularCode, building open-source ModularAdmin