DEV Community

Cover image for Under the Hood of React Router in 40 Lines of Code
Zachary Lee
Zachary Lee

Posted on • Originally published at webdeveloper.beehiiv.com on

Under the Hood of React Router in 40 Lines of Code

If you’ve built React apps, you’ve definitely used React Router. It’s a feature-rich routing library, so what’s under the hood?


Let’s first look at the requirements. Our main feature is that we don’t want to refresh the page when jumping routes, instead just update the component. Then there is a popstate event in the browser that can help us. React Router also uses this API. So here's the final toy:

import { type ReactNode, useEffect, useState } from 'react';

interface RouterProps {
  routes: { path: string; component: ReactNode }[];
  fallback: ReactNode;
}

export function Router({ routes, fallback }: RouterProps) {
  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((route) => route.path === currentPath)?.component ?? fallback
  );
}

export function navigate(href: string) {
  window.history.pushState(null, '', href);

  const navEvent = new PopStateEvent('popstate');
  window.dispatchEvent(navEvent);
}
Enter fullscreen mode Exit fullscreen mode

Let’s look at the main Router component first. We use useState and useEffect provided by React and bind the popstate event on window, which modifies the currentPath state once triggered, and then React will find the path corresponding component from routes and show it.

Simple, right, but the popstate event has some features to mention. Take a look at the navigate function below: First, we use history.pushState() to add an entry to the browser's session history stack, the specific syntax parameters can be seen on MDN. Then we actively created an instance of PopStateEvent and actively triggered it using window.dispatchEvent, why do that?

This is because just calling history.pushState() or history.replaceState() will not trigger the popstate event. The popstate event will be triggered by performing a browser action, such as clicking the back or forward buttons (or calling history.back() or history.forward() in JavaScript). So we need to trigger it manually so that the listener events in the Router component can be responded to (The listencallback is used in the source code of React Router).

Here is a simple use case:

import { navigate, Router } from './router';

const Home = () => (
  <>
    Home
    <button onClick={() => navigate('/about')}>About</button>
  </>
);
const About = () => (
  <>
    About
    <button onClick={() => navigate('/')}>Home</button>
  </>
);
const Exception404 = () => (
  <>
    404
    <button onClick={() => navigate('/')}>Home</button>
  </>
);

const routes = [
  { path: '/', component: <Home /> },
  { path: '/about', component: <About /> },
];

function App() {
  return <Router routes={routes} fallback={<Exception404 />} />
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You can try it online: click the buttons to switch routes and see the components change.


It is worth mentioning that React Router uses the hashchange event under HashRouter to monitor changes in routes, It can also implement the feature of updating components without refreshing the page when jumping routes. Of course, neither HashRouter or HistoryRouter are inseparable from the use of Location and History API.

If you found this helpful, please consider subscribing to my newsletter for more useful articles and tools about web development. Thanks for reading!

Top comments (0)