[logo] a small computer

Building a sidebar with routing functionality with react-router

building a sidebar with routing functionality with react-router

Bradley Kingsley
Published 10 months ago.
15 minute read
logo of undefined

Introduction

Sidebars are perhaps the most ubiquitous components in modern web app development. Almost every app built today requires it in some capacity. This applies to applications as complex as analytics dashboards to common applications like user-facing SaaS dashboard.

If that's the case, why not opt for a plug-and-play solution like react-sidenav? It's simpler and faster than rolling out your own solution, after all.

All it takes is reading the documentation, tinkering around with the library and hey-presto - a fully-functional sidebar.

The simplest reason I can give is that most sidebars I've come across don't reach the absolute minimum threshold for UI/UX that a sidebar should have. Keep in mind, these are all completely subjective (and perhaps overly pedantic to a certain group of people.)

First, let's outline a few guiding principles.

Principles for a great UI/UX sidebar

Nevertheless, the most satisfying UI/UX experiences I've had when using sidebars have been as a result of a few core principles:

  • The sidebar should be a navigation component.
  • The sidebar should remember its navigation state.
  • The sidebar should not overflow

1. The sidebar should be navigation component, i.e. clicking any item takes you to a new route

This might be a matter of preference, but once a user clicks on a sidebar item, it should render a new route. That is, as opposed to using a state management system like Redux to display different components depending on what the user selected.

The best part about implementing a sidebar this way is that users aren't broken out of the normal flow they are probably used to when using a browser. For instance, they can take advantage of built-in browser functionality like the back & forward buttons.

Here is an example of a sidebar that isn't linked to the browser's navigation history:

2. The sidebar needs to remember its navigation state.

Being linked to the navigation alone isn't enough. A proper sidebar should also 'remember' its navigation state once a user refreshes the page.

Here is what it looks like when the sidebar doesn't remember the current state, i.e. when the browser refreshes, the sidebar goes back to the original page.

It's a major pet peeve of mine and perhaps the main issue I've had with most sidebar react libraries.

The same issue can also be seen when you navigate to other routes using the 'back' or 'forward' buttons.

In this example, despite the route changing when the 'back' and 'forward' buttons are used, they don't affect the currently selected item on the sidebar.

Incidentally, this is somewhat difficult to get dead right, and probably the whole reason I wrote this blog post. I struggled a bit before I was able to implement it in a way I thought was satisfying.

3. The sidebar mustn't overflow.

It has to be sticky.

Overflowing sidebars are just bad for user experience. With an overflowing sidebar, if you're all the way at the bottom of the page and need to redirect to a different route, you have to scroll back to the top first.

This is also surprisingly hard to achieve, but I'll show you how with some CSS trickery.

Here's an example of an overflowy sidebar:

a beautiful but tragically overflowing sidebar

Before we get started

A lot of other other libraries (eg. react-burger-menu have a ton of features I'll obviously be too lazy to add (animations, etc). Make no mistake, our project is going to be awesome, too, but don't expect too much flashiness (Just a not-too-much-but-still-kinda-lots amount because I love to get carried away with these tutorials :D).

PS:

  • If you just want the code, it's right here.
  • I'm not sure how many people will be interested in a demo, but I'll put one up as soon as I can. If it's been more than a week since I published this and there's still no sign of one, give me a nudge by email or on Twitter . It'lll either be because I forgot or I've been my usual lazy self.)

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. It contains both Nav, Sidebar and imports Routes
│   │   ├── 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.

react sidebar empty props sidebar

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! emoji-confetti_ball emoji-confetti_ball

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! Now we have a perfectly good react sidebar with routing functionality and 'state awareness',

    react-sidebar-final.gif

    Cheers!

Copyright © 2021 The Kenyan Dev