[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 5 months ago.
14 minute read
logo of undefined

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 an analytics dashboard to the more common 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. The only other thing it really needs is reading the documentation in order to implement it.

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.)

Nevertheless, the most satisfying UI/UX experiences I've had when using sidebars in general have been thanks to:

The sidebar is linked to navigation. Clicking any item takes you to a new route:

This might be a matter of preference, but changing the route once an item is selected (and rendering a new route) feels like a much more flexible way of building apps. That is, as opposed to using Redux or whatever and having a giant switch statement to redirect you to different components.

The best part about implementing a sidebar this way is that any user can then take advantage of built-in browser functionality (like the back button).

Here is an example of a sidebar that doesn't retain state after the route has changed:

The sidebar needs to 'remember' its state:

Here is what it looks like when the state isn't remembered and the page is refreshed.

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.

Aside from a page refresh, the same 'state reset' also happens when you click the back or forward buttons in the browser. Any route change leads to a state reset. We don't want that.

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.

The sidebar mustn't overflow

It has to be sticky.

Overflowing sidebars are just bad for user experience. An overflowing sidebar means 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:

  • styled-components - makes styling easier
  • react-router-dom - for routing (obviously)

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 4.

The fifth page I didn't mention is just a 404 page (because :man_shrugging:)

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 we 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;

Nothing too foreign there.

Here's what the Layout component is going to look like:

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

I'm not very good with colors. I rely on this handy tool when I want to get something that always looks good. Let's go with a deep shade of red (#c34a36). I think that's too overlooked.

Let's lay the foundation for what we want the sidebar to look like:

  • at least 250px
  • should always fit the screen height (which means displacing the nav)

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'

function Sidebar() {
    return (
        <>
            <SidebarParent>
                <h2>I'm the sidebar.</h2>
                <p>Love me. Or else.</p>
            </SidebarParent>
        </>
    );
}

export default Sidebar;

const SidebarParent = styled.div`
  background: #c34a36;
  width: 250px;
  height: 100vh;
`;

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 way of telling you Don't Repeat Yourself.

Listing all the items like this is fine if you don't plan on using the code in other parts of your application but it's good practice to absract it a bit more in either case. So, let's do just that.

In a new file:

SidebarItems.js

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 nor 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.

The solution I eventually came up with looks like this:

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>
    )
}

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 (making it active). We need to make it not do that. But that's the least of our problems.

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.

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:

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 : fixed and absolute break the flow of the page so everything will jump around the page if you don't make some slight changes.

    Using position:fixed on the sidebar and margin-left:auto on the rest of the body will fix the sidebar and move the body content out of the way. margin-left:auto works in this case because of peculiar flexbox magic

    This solution is horribly unresponsive and adding that functionality isn't as easy as it would be with other solutions. If you just want something quick and dirty, though, this 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 sticks to a certain area of the screen while still behaving like a 'relative' element. This means styling using flexbox or something similar 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 the sticky property . 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 we do it:

    We need to slightly modify the sidebar. First, we 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 the SidebarParent looks like:

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

    All this does is to give every direct child a width of '250px' (that includes the skeleton.

    Now, if we make our sidebar fixed, it breaks the flow of the page. However, since the skeleton is part of the same parent, it moves 'underneath' the sidebar, allowing SidebarParent to retain its 250px width! This way, we don't need any extra styling to make the rest of the page play nice.

    Do note that this also poses it's 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 © 2020 The Kenyan Dev