Crafting a Minimalist React Router in Just 45 Lines

React has always been the de facto choice for building single page applications. React doesn’t update the dom by itself. It uses React DOM for accomplishing it. Similarly there is no one solution for managing state or handling routes unlike Vue JS. Which is a good thing also a bad thing depending upon how you see it.

When it comes to client side routing, react router dom comes to mind. Thanks to Ryan Florence, Michael Jackson and teams for building such a library. What most people forget is that there exists other client side routing solutions in the react ecosystem. For example -

  • molefrog/wouter: A minimalist-friendly ~1.5KB routing for React and Preact. Nothing else but HOOKS.
  • krasimir/navigo: A simple vanilla JavaScript router.
  • franciscop/crossroad: A React library to handle navigation in your webapp.

So I thought of building a simple react routing library by myself as a challenge to dig deeper and to learn the internals.

I tried to mimic React Router’s best practices by providing familiar Route and Link components.

Create a lib/router.js file which will contain all our routing logic. Import useState and useEffect from react.

import React, { useState, useEffect } from 'react';

Let’s first create a navigate function that will manipulate the browser’s history and then I will explain what it does.

export const navigate = (href) => {
  window.history.pushState({}, '', href);

  const navEvent = new PopStateEvent('popstate');
  window.dispatchEvent(navEvent);
};

The window.history.pushState() method adds an entry to the browser’s session history stack. It takes three arguments i.e. state, unused and url.

State is a javascript object that contains a copy of the history entry’s state object. Unused is there for historical reasons, an empty string won’t hurt. The third parameter takes a url string of the same origin that you want to navigate to. Calling pushState() is similar to setting window.location = "#foo", in that both will create and activate another history entry.

Now Link is one of the components that you use most frequently instead of a tag. It uses a tag behind the scenes.

export const Link = ({ className, href, children }) => {
  const onClick = (e) => {
    if (e.metaKey || e.ctrlKey) return;
    e.preventDefault();
    navigate(href);
  };
  return (
    <a className={className} href={href} onClick={onClick}>
      {children}
    </a>
  );
};

ctrl/cmd + click on a link opens in new tab. Since react is a single page application we do noting. On the other hand, at first we prevent the default behavior of link by calling preventDefault on event object e. And then we call the navigate function manually.

The big elephant in the room is Router component.

const Router = ({ routes, defaultComponent }) => {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  useEffect(() => {
    const onLocationChange = () => {
      setCurrentPath(window.location.pathname);
    };

    window.addEventListener('popstate', onLocationChange);

    return () => {
      window.removeEventListener('popstate', onLocationChange);
    };
  }, []);

  return (
    routes.find(({ path }) => currentPath == path)?.component ||
    defaultComponent
  );
};

Router takes routes and defaultComponent. When there is a change in URL by Router returns the required component or else return the default component if it doesn’t match with any listed components. We also have a clean up function in the useEffect the remove the event listener on unmount of the Router component.

Usage

import React from 'react';
import Router, { Link } from './lib/router';
import Counter from './components/Counter';
import Hello from './components/Hello';
import './style.css';

const NotFound = () => {
  return <h1> 404! Not found</h1>;
};

export default function App() {
  const routes = [
    {
      path: '/hello',
      component: <Hello />
    },
    {
      path: '/counter',
      component: <Counter />
    }
  ];
  const defaultComponent = <NotFound />;
  return (
    <div>
      <ul>
        <li>
          <Link href="/counter">Counter</Link>
        </li>
        <li>
          <Link href="/hello">Hello</Link>
        </li>
      </ul>

      <Router routes={routes} defaultComponent={defaultComponent} />
    </div>
  );
}

Full code is available in here and here.