[logo] a small computer

Building a React Sidebar Menu Navigation Component with React-Router

Building a React Sidebar Menu Navigation Component with React-Router

Bradley Kofi
Published 05/05/2020.
7 minute read
logo of undefined

Table of Contents

Introduction

The sidebar is perhaps the most ubiquitous component in modern web app development. Almost every app built today uses it in some capacity.

For that reason, it's be useful to have a set of conventions in place that should at least be considered when building a sidebar navigation component for a modern web application.

This article walks the reader through three principles that make for a "usable" (UX/UI-wise) sidebar. It goes on to demonstrate how a usable sidebar component (with navigation functionality, of course) can be built from the ground up using React and React-Router.

The Sidebar - A Brief Introduction

A "sidebar" in web development is a "section" or "bar" on the side of the page that typically holds navigation links to help a user find their way around an application. (yes, English words are often outrageously intuitive).

The structure of a simple sidebar (including the dashboard) can be broken down as follows:

It consists of:

  1. A title. This is usually a logo.
  2. A set of sidebar items. These act as "links" (both figuratively and literally) to pages/views.
  3. The active page/view. Only one view can be active at a time.

It's nice to have a little idea of "structure" when building web application components. Everything should be placed such that its location, relative to the location of other components, makes sense. We'll dive further into the idea of structure when we explore nested sidebar items.

Now that we have some structure, let's outline some UI/UX principles to adhere to when building sidebars.

UI/UX Principles for Building a Sidebar

When building a sidebar, we should keep three basic principles in mind:

  • Sidebar items should be links..
  • The sidebar should remember its navigation state. The sidebar should always the active page,
  • The sidebar should not overflow. That is, it should never exceed the height of the user's screen.

Let's explore each of these points in further detail.

Links are a browser's way of "linking" pages together (it's in the name ๐Ÿ˜„). Because they are a browser standard, they come with some nice features that links implemented with Javascript don't have.

Links are automatically added to a user's browsing history. This gives them two important advantages that ensure users have a "consistent" browsing experience on your website.

  • Users can return to a specific point in your app at any time by visiting their history, or right-clicking on the "back" button.
  • Context-clicking (or left-clicking, if you prefer) on the "back" button on a "linked" page allows users to navigate to the previous page.

Here is an example of a sidebar that doesn't use links

2. The sidebar should remember its navigation state.

Unfortunately, using links alone isn't enough. A sidebar should also keep track of the active navigation item. This way, the user always knows where they are, even when the page is refreshed.

Here is a sidebar that doesn't keep track of its current state.

Note that when the browser refreshes, the sidebar goes back to the original page.

Here's another sidebar that doesn't properly integrate the 'back' or 'forward' browser buttons.

In this example, note that the current page changes when a sidebar item is clicked, but browser navigation does not affect the currently-active item.

3. The sidebar mustn't overflow.

It has to be sticky.

Overflowing sidebars are terrible for user experience. An overflowing sidebar forces a user who is at the bottom of a page to scroll back to the top in order to access the sidebar items.

Here's an example of an overflowy sidebar:

a beautiful but tragically overflowing sidebar

Before we get started

If you're just after the code just want the code, find it on Github.

Prerequisites

We're going to need the following dependencies:

You can install them by running

npm install react-router-dom styled-components --save // or yarn add react-router-dom styled-components

That is, of course, assuming you already have a project up and running. If not, it's as easy as

npx create-react-app react-sidebar-project

Then

npm install react-router-dom styled-components --save

With that, let's get started!

Setting up react-router

First, we're going to set up four different dummy pages so we can route through all of them from a single component.

Each of them looks something like this:

Page-1.jsx
import React from 'react'; function Page1(props) { return ( <div> <h1>Page 1</h1> <p>I bring the sauce.</p> </div> ); } export default Page1;

Up to page 3.

The fourth page I didn't mention is just a 404 page (because ยฏ_(ใƒ„)_/ยฏ)

It looks like this:

NotFound.jsx
import React from 'react'; const NotFound = props => { return ( <div> <p>You took a wrong turn, mate.</p> </div> ); }; export default NotFound;

The other three files I didn't mention are:

  • Nav.jsx which is relatively unimportant.
  • Sidebar.jsx where 3/4 of the magic happens.
  • Layout.jsx which is basically the parent component.

Here's the folder structure:

. โ”œโ”€โ”€ package.json โ”œโ”€โ”€ public // You can safely ignore these โ”‚ โ”œโ”€โ”€ favicon.ico โ”‚ โ”œโ”€โ”€ index.html โ”‚ โ”œโ”€โ”€ logo192.png โ”‚ โ”œโ”€โ”€ logo512.png โ”‚ โ”œโ”€โ”€ manifest.json โ”‚ โ””โ”€โ”€ robots.txt โ”œโ”€โ”€ src โ”‚ โ”œโ”€โ”€ components โ”‚ โ”‚ โ”œโ”€โ”€ Layout.jsx // This is the 'parent' component. โ”‚ โ”‚ โ”œโ”€โ”€ Nav.jsx โ”‚ โ”‚ โ”œโ”€โ”€ Sidebar.jsx โ”‚ โ”‚ โ””โ”€โ”€ SidebarItems.jsx โ”‚ โ”œโ”€โ”€ index.css โ”‚ โ”œโ”€โ”€ index.js // React is initialized through this file โ”‚ โ”œโ”€โ”€ pages โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard.jsx โ”‚ โ”‚ โ”œโ”€โ”€ NotFound.jsx โ”‚ โ”‚ โ”œโ”€โ”€ Page-1.jsx โ”‚ โ”‚ โ”œโ”€โ”€ Page-2.jsx โ”‚ โ”‚ โ””โ”€โ”€ Page-3.jsx โ”‚ โ””โ”€โ”€ routes.js // we define our routes here โ””โ”€โ”€ yarn.lock

A central theme of all dashboards is that the sidebar should remain the same throughout the whole application. That means that any time the route changes, the sidebar shouldn't be affected.

Here's how to do it with react-router.

routes.jsx
import React from "react"; import {BrowserRouter, Route, Switch} from "react-router-dom"; import Page1 from "./pages/Page-1"; import Page2 from "./pages/Page-2"; import Page3 from "./pages/Page-3"; import Dashboard from "./pages/Dashboard"; import NotFound from "./pages/NotFound"; function Routes() { return ( <BrowserRouter> <Switch> <Route path="/" exact component={Dashboard}/> <Route path="/page-1" component={Page1}/> <Route path="/page-2" component={Page2}/> <Route path="/page-3" component={Page3}/> <Route component={NotFound}/> </Switch> </BrowserRouter> ) } export default Routes;

If you're familiar with react-router, there's nothing too foreign there.

Let's move on to Layout.jsx:

Layout.jsx
import React from 'react'; import Routes from "../routes"; import Sidebar from "./Sidebar"; function Layout(props) { return ( <div> <p>I'm the daddy</p> <div> <Sidebar/> <Routes/> </div> </div> ); } export default Layout;

Don't worry about the sidebar for now - it's just an empty shell.

Here's what we have so far:

react-sidebar-no-styling.png

Styling the sidebar

Since I'm not very good with colors, I rely on this handy tool whenever I need a set of colors that always looks good. Let's go with a deep shade of red (#c34a36) that's too overlooked.

Next, we need a foundation for what we want the sidebar to look like:

  • It should be at least 250px wide.
    • Sidebars typically don't resize for aesthetic reasons.
  • It should always fit the screen height.
    • i.e. it shouldn't overflow, it should be fixed

After a bit of CSS:

Layout.jsx
import React from 'react'; import Routes from "../routes"; import Sidebar from "./Sidebar"; import Nav from "./Nav"; function Layout(props) { return ( <div> <div style={{display: "flex"}}> <Sidebar/> <div> <Nav/> <Routes/> </div> </div> </div> ); } export default Layout;
Sidebar.jsx
import React from "react"; import styled from 'styled-components' const SidebarParent = styled.div` background: #c34a36; width: 250px; height: 100vh; `; function Sidebar() { return ( <> <SidebarParent> <h2>I'm the sidebar.</h2> <p>Love me. Or else.</p> </SidebarParent> </> ); } export default Sidebar;

Adding some sidebar items

The boring and basic way to do this is simply including all the sidebar routes inside your Javascript file:

Sidebar.jsx
function Sidebar() { return ( <> <SidebarParent> <SidebarItem> <p>Dashboard</p> </SidebarItem> <SidebarItem> <p>Page 1</p> </SidebarItem> <SidebarItem> <p>Page 2</p> </SidebarItem> <SidebarItem> <p>Page 3</p> </SidebarItem> </SidebarParent> </> ); }

But I am lazy and I'll assume you're a decent programmer. Being a decent programmer, one of the things youll want to achieve is DRY code. It's a clever acronym that stands for Don't Repeat Yourself. I won't get into too many details, but DRY code is easier to maintain and enables reusability.

Hard-coding all the items is fine if you don't plan on using the code in other parts of your application, but creating an abstraction on top of repetitive code is good practice. So, let's do just that.

In a new file:

SidebarItems.jsx
const SidebarItems = [ { name: "Dashboard" }, { name: "Page 1" }, { name: "Page 2" }, { name: "Page 3" }, ]; export default SidebarItems;

And our sidebar changes to:

Sidebar.jsx
function Sidebar() { return ( <> <SidebarParent> { SidebarItems.map(item=> ( <SidebarItem key={item.name}> <p>{item.name}</p> </SidebarItem> )) } </SidebarParent> </> ); }

Using styled-components, we can churn out the following sidebar

const SidebarParent = styled.div` background: #cf3d2a; width: 250px; height: 100vh; `; const SidebarItem = styled.div` padding: 16px 24px; transition: all 0.25s ease-in-out; //Change the background color if 'active' prop is received background: ${props => props.active ? "#b15b00" : ""}; margin: 4px 12px; border-radius: 4px; p { color: white; font-weight: bold; text-decoration: none; } &:hover { cursor:pointer; } &:hover:not(:first-child) { background: #c34a36; } `;

And the actual logic:

function Sidebar({defaultActive}) { //If no active prop is passed, use `1` instead const [activeIndex, setActiveIndex] = useState(defaultActive || 1); return ( <> <SidebarParent> { SidebarItems.map((item, index)=> { return ( <SidebarItem key={item.name} active={index === activeIndex}> <p>{item.name}</p> </SidebarItem> ); }) } </SidebarParent> </> ); }

And with that, the easy part if over! Now we have something respectable:

A Sidebar Momma Can Be Proud Of

Abstracting the sidebar in this way is useful because if you ever want to, say, change the background color of the sidebar item, you only have to do it in a single place. In addition, think about how much easier it will be to use an abstracted component if the sidebar items are dynamic and need to be loaded from the server.

Adding routing functionality to the sidebar

Next up, we need to get to the core functionality of our app: redirect users to different routes depending on what's been clicked.

Since we're using react-router, it might be tempting to just call useHistory and get it over with, but it's important to keep in mind how our document is structured.

Look at the graph below:

Notice how only Dashboard and Page 1 receive the location props. Indeed, if we tried to log the incoming props, you'll notice the Layout and Sidebar props are empty.

This puts us in a pickle because we need access to those props inside the sidebar!

Let's take a step back and rewrite our route component so the prop is somehow passed down.

function Routes() { return ( <BrowserRouter> <Route render={(props)=>( //Layout and sidebar can now receive props <Layout {...props}> <Switch> <Route path="/" exact component={Dashboard}/> <Route path="/page-1" component={Page1}/> <Route path="/page-2" component={Page2}/> <Route path="/page-3" component={Page3}/> <Route component={NotFound}/> </Switch> </Layout> )}/> </BrowserRouter> ) }

Next, our sidebar needs to know where each item should redirect the user to

SidebarItems.jsx
const SidebarItems = [ { name: "THE ITALIAN JOB", route: '/' }, { name: "Dashboard", route: '/dashboard', }, { name: "Page 1", route: '/page-1' }, { name: "Page 2", route: '/page-2' }, { name: "Page 3", route: 'page-3' }, ]; export default SidebarItems;

After making a minor change to our Sidebar.js file:

function Sidebar(props, {defaultActive}) { const [activeIndex, ] = useState(defaultActive || 1); return ( <> <SidebarParent> { SidebarItems.map((item, index)=> { return ( <Link to={item.route}> //redirect users to a new route <SidebarItem key={item.name} active={index === activeIndex}> <p>{item.name}</p> </SidebarItem> </Link> ); }) } </SidebarParent> </> ); }

Our app is navigation-ready! ๐ŸŽŠ ๐ŸŽŠ

The first part of our not-so-complex app is done. Let's move on to a slightly more complicated problem.

'Remembering' state

Here is the current problem:

Every time the page is refreshed, the sidebar resets to 'index 0' making it highlight the first item on the list. We need to make it not do that.

We also need to synchronize the sidebar and the url so that heading to '/page-2' on the url bar also redirects us to the correct route on the sidebar.

Once this step is completed, our sidebar should look something like this:

Believe it or not, all the changes we need to make are going to be right within the Sidebar.js file. Magical, isn't it?

Here's what the code for our new sidebar looks like:

Sidebar.jsx
function Sidebar(props, {defaultActive}) { //Those location props finally come in handy! const location = props.history.location; //Load this string from localStorage const lastActiveIndexString = localStorage.getItem("lastActiveIndex"); //Parse it to a number const lastActiveIndex = Number(lastActiveIndexString); //Store it in state const [activeIndex, setActiveIndex] = useState(lastActiveIndex || Number(defaultActive)); //This sets the item to localStorage and changes the state index function changeActiveIndex(newIndex) { localStorage.setItem("lastActiveIndex", newIndex) setActiveIndex(newIndex) } //Appends '/' to the start of a string if not present function getPath(path) { if (path.charAt(0) !== "/") { return "/" + path; } return path; } //This re-renders when the route changes useEffect(()=> { //Get an item with the same 'route' as the one provided by react router (the current route) const activeItem = SidebarItems.findIndex(item=> getPath(item.route) === getPath(location.pathname)) changeActiveIndex(activeItem); }, [location]) //... }

(The above code represents my thought process, not necessarily the best way to go about it. Notice how localStorage isn't even necessary at this point! It was the first idea I had but ultimately ended up being useless. The final code won't include it)

I hope you've been having fun because it's now crunch time. We need to get rid of annoying overflowy text :/.

Making the sidebar fixed (getting rid of overflow)

Here's what we don't want:

In other words, we want the sidebar to be fixed. If your instincts tell you to slap on position:fixed and move on, don't listen to it. Or at least not exactly.

There are a couple of ways to fix this problem, so let's cover a few of them:

  • Using position:fixed : The css properties position:fixed and position:absolute break the flow of the page. Elements on the page will jump around if you use them as-is.

    Using position:fixed on the sidebar and margin-left:auto on the rest of the body will fix the sidebar in place and align the body to the right.

    Note: margin-left:auto works this way because of peculiar flexbox magic. It's a useful trick that will definitely come in handy.

    However, this solution is horribly unresponsive, and implementing the same is 10x harder than other solutions. If you just want something quick and dirty, though, it should work fine.

  • Using position:sticky: A better solution would be to use position:sticky and top:0. This would make the sidebar 'sticky'.

A 'sticky' element, hmm..., sticks to a certain area of the screen while still behaving like a 'relative' element. In other words, it doesn't break the flow, and styling using flexbox or grid will work. I personally prefer this solution because responsiveness became 1000x easier.

The major issue with this is probably browser compatibility and a few quirky quirks around using both sticky and overflow together . It doesn't always work and debugging it can be a real pain in the butt.

  • If you take issue with either one of those solutions, I offer you a slightly more hacky and convoluted way to do it but always guaranteed to work.

    Here's how to do it:

    We need to modify the sidebar slightly.

    First, create a SidebarParent component using styled-components and give it a two children - a 'skeleton' div and another containing our actual sidebar items. The skeleton div's usefulness will become apparent in a moment.

    <SidebarParent> <div> //Sidebar items </div> <div className="im-a-skeleton"/> </SidebarParent>

    This is what SidebarParent looks like:

    //SidebarParent.jsx a { text-decoration: none; } & > div { width: 250px; height: 100vh; }

    This gives every direct child of SidebarParent a fixed width of '250px' (including our skeleton div)

    If we make our sidebar fixed, it breaks the flow of the page. However, since the skeleton is part of the same parent and its position is 'block' (by default), it moves 'underneath' the now flow-broken sidebar. (Alternatively, you can think of it as the Sidebar moving 'on top' of the skeleton.)

    Without the skeleton, the rest of the dashboard would be what would occupy the space left behind by the sidebar. We don't need any extra styling to make the rest of the page play nice.

    Do note that this also poses its own complications - the sidebar will slide on top of the content if the screen is small enough (fixed width, remember?) - but it's ultimately up to you to decide how to implement it.

    Conclusion

    Voila! In this article, we've explored how to make a dynamic react sidebar with routing functionality and 'state awareness'.

    react-sidebar-final.gif

    Cheers!

Updates

  • 28/01/22 - 13:40 - Changed title
  • 28/01/22 - 13:51 - Tinkered with introduction
  • 26/06/22 - 1601 - Updated introduction

Liked this article? Consider leaving a donation. ๐Ÿ˜‡

Loved this article? Consider leaving a donation to help keep this ambitious fish swimming.

Buy Me a CoffeeDonate with Paypal

btc: bc1qedugpzcgutcmm7qefkhc25eh5dwrwsz7dyleg3

Copyright ยฉ 2022 Bradley K.