Create a Modern Dynamic Sidebar Menu in React Using Recursion

Christopher T.

November 13th, 2019

Share This Post

In web pages, sidebars in web pages are amongst one of the most useful components that exist in the page due to their navigational functionality.

Today we will be building a modern sidebar in react using recursion. Recursion is a technique in which a function simply calls itself repeatedly until a condition has been met. The three rules of recursion applies when using recursion in this post:

  1. The function should have a condition that self destructs itself
  2. The function should have a base condition
  3. The function should be calling itself

Sidebars are indeed essential to a web page, even if the level of its level of attention does not come first. This is because they can help users navigate in different ways, such as content that they may be interested in as opposed to a logical navigational menu.

But why would we even want to use recursion for sidebars? What difference does it make as opposed to writing out your sidebar items manually? If you've browsed through the internet for awhile, you might have come accross a website's sidebar and realized that some sidebar items have subsections. Some sites have sidebars that hide or render certain items based on the page route the user navigated to. That is powerful!

For example, if we look at the image below inside the red circle, the Editors part is an item of the sidebar, and the 3 items following immediately below (Code Editor, Markdown, Text Editor) are the subsections:

sidebar example subsections 3 items

You will see by the end of this post that this seemingly complicated sidebar is actually under 50 lines of code! What?!

Here is a basic example of how you can extend the sidebar component from this post to be a little more stylish while still retaining the clean feel of it:

modern dynamic sidebar in react using recursion extended

Without further ado, let's get started!

In this tutorial we are going to quickly generate a react project with create-react-app.

(If you want to get a copy of the repository from github, click here).

Go ahead and create a project using the command below. For this tutorial i’ll call our project modern-sidebar.

npx create-react-app modern-sidebar

Now go into the directory once it's done:

cd modern-sidebar

Inside the main entry src/index.js we're going to clean it up a bit so we can focus on the component alone:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles.css'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(<App />, document.getElementById('root'))

serviceWorker.unregister()

Now create src/App.js:

import React from 'react'

const App = () => <div />

export default App

App will be importing and using our Sidebar component by creating Sidebar.js, so lets go ahead and create that:

import React from 'react'

function Sidebar() {
  return null
}

export default Sidebar

Now i'm going to install a CSS library, but you can actually achieve the same working functionality of the sidebar that we will be building without it. The reason I'm doing this is because I like seeing the additional ripple effects in addition to having icons readily available to use :)

npm install @material-ui/core @material-ui/icons

Once that is installed, we need to think of a base structure in the user interface that our sidebar will be built upon. A solution is to use the unordered list (<ul>) element that renders list items (<li>). We will import List and ListItem from @material-ui/core since the List component is essentially a ul element, and the ListItem component is essentially a li.

Lets start off hardcoding a couple of items in the sidebar to visualize how this might look like to boost our confidence. Sometimes a little extra confidence can help improve our productivity:

import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar() {
  return (
    <List disablePadding dense>
      <ListItem button>
        <ListItemText>Home</ListItemText>
      </ListItem>
      <ListItem button>
        <ListItemText>Billing</ListItemText>
      </ListItem>
      <ListItem button>
        <ListItemText>Settings</ListItemText>
      </ListItem>
    </List>
  )
}

export default Sidebar

(disablePadding and dense were used to slightly shrink the size of each of the items, and the button prop was used to add the stunning ripple effect).

This is what we have so far:

sidebar items list items hardcoded in react

Now that we have boosted our confidence, let's go ahead and define props.items, which Sidebar will consume to render its items.

With that said, we're also going to expect an items prop that is an array of objects representing each item in the sidebar menu. We want to keep the functionality as simple as possible or else we could quickly overcomplicate the component.

Lets first create items in the App component and pass it as props.items to Sidebar:

import React from 'react'
import Sidebar from './Sidebar'

const items = [
  { name: 'home', label: 'Home' },
  { name: 'billing', label: 'Billing' },
  { name: 'settings', label: 'Settings' },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

export default App

We will now update the Sidebar component to reflect this array structure:

import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar({ items }) {
  return (
    <List disablePadding dense>
      {items.map(({ label, name, ...rest }) => (
        <ListItem key={name} button {...rest}>
          <ListItemText>{label}</ListItemText>
        </ListItem>
      ))}
    </List>
  )
}

export default Sidebar

One thing you might have noticed is that our sidebar is just too dang big! Sidebars usually take up one side of the screen. So what we are going to do is shrink its width to a suitable size. We will go ahead and put a max-width of 200px on it. So we're going to create a div element that wraps our List component.

The reason why we create another div element instead of directly applying the styles on the List component is because we don't want to make List responsible for the width size. This way, in the future we can choose to abstract the List into a reusable sidebar component where it is able to adapt to any size depending on the size of the parent element:

Here is the Sidebar.js component:

import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, ...rest }) => (
          <ListItem key={name} button {...rest}>
            <ListItemText>{label}</ListItemText>
          </ListItem>
        ))}
      </List>
    </div>
  )
}

export default Sidebar

And inside index.css we defined the css styles for the sidebar class:

.sidebar {
  max-width: 240px;
  border: 1px solid rgba(0, 0, 0, 0.1);
}

Material-UI actually uses their own CSS styling mechanism using the CSS-in-JS approach. But we will stick to regular CSS in this article to keep things unnecessarily complicated.

We can already just leave it as basic as this and call it a day. However, it doesn't support sub items. We want to be able to click on a sidebar item and have it drop down its list of sub items if it has any. Having sub items helps organize the sidebar by grouping additional items within another sidebar section:

sidebar items with sub items in react

The way we are going to support this feature is to allow another option inside each sidebar item that the component will use to detect for its sub items. (Can you feel the recursion coming?)

Let's change up our items array in the App component to pass in sub items:

import React from 'react'
import Sidebar from './Sidebar'

const items = [
  { name: 'home', label: 'Home' },
  {
    name: 'billing',
    label: 'Billing',
    items: [
      { name: 'statements', label: 'Statements' },
      { name: 'reports', label: 'Reports' },
    ],
  },
  {
    name: 'settings',
    label: 'Settings',
    items: [{ name: 'profile', label: 'Profile' }],
  },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

export default App

To be able to render a sidebar item's subitems, we'd have to watch for the items property when rendering sidebar items:

function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => (
          <ListItem style={{ paddingLeft: 18 }} key={name} button {...rest}>
            <ListItemText>{label}</ListItemText>
            {Array.isArray(subItems) ? (
              <List disablePadding>
                {subItems.map((subItem) => (
                  <ListItem key={subItem.name} button>
                    <ListItemText className="sidebar-item-text">
                      {subItem.label}
                    </ListItemText>
                  </ListItem>
                ))}
              </List>
            ) : null}
          </ListItem>
        ))}
      </List>
    </div>
  )
}

And now... behold, our dazzling sidebar component!

dazzling react sidebar component maybe not

If you haven't caught on already, this is not the sidebar look that we want to achieve.

Now since we don't want our users to hit their close button on their browser and never come back to our web site, we need to figure out a way to make this look more appealing not only to the eyes, but to the DOM as well.

"What do you mean the DOM", you ask?

Well, if you look closely, there's a problem! If the user clicks on a sub item, the parent item rendering the sub item is also consuming the click handler, since they are overlapping! This is bad and calls upon some nasty unexpecting issues for the user's experience.

What we need to do is separate the parent from its children (the sub items) so that they render their sub items adjacently, so that mouse events do not clash:

function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => (
          <React.Fragment key={name}>
            <ListItem style={{ paddingLeft: 18 }} button {...rest}>
              <ListItemText>{label}</ListItemText>
            </ListItem>
            {Array.isArray(subItems) ? (
              <List disablePadding>
                {subItems.map((subItem) => (
                  <ListItem key={subItem.name} button>
                    <ListItemText className="sidebar-item-text">
                      {subItem.label}
                    </ListItemText>
                  </ListItem>
                ))}
              </List>
            ) : null}
          </React.Fragment>
        ))}
      </List>
    </div>
  )
}

Now we're almost back in business!

dazzing sidebar in react maybe so

From the screenshot, it seems as though we have a new problem: the sub items are awkwardly larger than the top level items. We must figure out a way to detect which ones are sub items and which ones are top level ones.

We can hardcode this and call it a day:

function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => {
          return (
            <React.Fragment key={name}>
              <ListItem style={{ paddingLeft: 18 }} button {...rest}>
                <ListItemText>{label}</ListItemText>
              </ListItem>
              {Array.isArray(subItems) ? (
                <List disablePadding dense>
                  {subItems.map((subItem) => {
                    return (
                      <ListItem
                        key={subItem.name}
                        style={{ paddingLeft: 36 }}
                        button
                        dense
                      >
                        <ListItemText>
                          <span className="sidebar-subitem-text">
                            {subItem.label}
                          </span>
                        </ListItemText>
                      </ListItem>
                    )
                  })}
                </List>
              ) : null}
            </React.Fragment>
          )
        })}
      </List>
    </div>
  )
}
.sidebar-subitem-text {
  font-size: 0.8rem;
}

But our sidebar component is supposed to be dynamic. Ideally we want it to generate its items accordingly to the items passed in as props from the caller.

We're going to use a simple depth prop that the sidebar items will use, and based on the depth they can adjust their own spacing accordingly to depth no matter how far down the tree they're in. We're also going to extract out the sidebar item into its own component so that we can increase the depth without having to complicate it with introducing state logic.

Here is the code:

function SidebarItem({ label, items, depthStep = 10, depth = 0, ...rest }) {
  return (
    <>
      <ListItem button dense {...rest}>
        <ListItemText style={{ paddingLeft: depth * depthStep }}>
          <span>{label}</span>
        </ListItemText>
      </ListItem>
      {Array.isArray(items) ? (
        <List disablePadding dense>
          {items.map((subItem) => (
            <SidebarItem
              key={subItem.name}
              depth={depth + 1}
              depthStep={depthStep}
              {...subItem}
            />
          ))}
        </List>
      ) : null}
    </>
  )
}

function Sidebar({ items, depthStep, depth }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map((sidebarItem, index) => (
          <SidebarItem
            key={`${sidebarItem.name}${index}`}
            depthStep={depthStep}
            depth={depth}
            {...sidebarItem}
          />
        ))}
      </List>
    </div>
  )
}

So what's going on here?

Well, we declared some powerful props to configure the sidebar pre-render phase such as depth and depthStep. SidebarItem was extracted out into its own component and inside its render block it uses depth to calculate its spacing. The higher the depth is, the more deep down in the tree they're located in.

That's all possible because of this line:

{
  items.map((subItem) => (
    <SidebarItem
      key={subItem.name}
      depth={depth + 1}
      depthStep={depthStep}
      {...subItem}
    />
  ))
}

depth gets incremented by 1 every time a new list of sub items goes deeper.

And the recursion exists inside SidebarItem because it calls itself until there is no longer a base case, in other words when the array is empty then this piece of code automatically stops:

{
  items.map((subItem) => (
    <SidebarItem
      key={subItem.name}
      depth={depth + 1}
      depthStep={depthStep}
      {...subItem}
    />
  ))
}

Lets test the recursionized sidebar component out now:

src/App.js

const items = [
  { name: 'home', label: 'Home' },
  {
    name: 'billing',
    label: 'Billing',
    items: [
      { name: 'statements', label: 'Statements' },
      { name: 'reports', label: 'Reports' },
    ],
  },
  {
    name: 'settings',
    label: 'Settings',
    items: [
      { name: 'profile', label: 'Profile' },
      { name: 'insurance', label: 'Insurance' },
      {
        name: 'notifications',
        label: 'Notifications',
        items: [
          { name: 'email', label: 'Email' },
          {
            name: 'desktop',
            label: 'Desktop',
            items: [
              { name: 'schedule', label: 'Schedule' },
              { name: 'frequency', label: 'Frequency' },
            ],
          },
          { name: 'sms', label: 'SMS' },
        ],
      },
    ],
  },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

dazzling sidebar in react recursionized

And there we have it!

Let's play with depthStep a little and pass in a higher value:

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

dazzling sidebar in react recursionized #2

Conclusion

You can optionally download the repo from the github link and see additional features of the sidebar. It features more fancy functionality such as adding an additional layer in rendering (sidebar sections) which leads to (dividers) as separators, sidebar expansion/collapsing, icons, etc.

I hope you found this to be valuable and look out for more in the future!


Tags


Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2020