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](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event) in the browser that can help us. React Router also uses this API. So here's the final toy:

```typescript
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);
}
```

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 [listen ](https://github.com/remix-run/history/blob/dev/packages/history/index.ts#L481)callback is used in the source code of React Router).

Here is a simple use case:

```javascript
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;
```

You can [try it online](https://stackblitz.com/edit/vitejs-vite-cxiapz?file=src%2FApp.tsx): 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](https://developer.mozilla.org/en-US/docs/Web/API/Location) and [History](https://developer.mozilla.org/en-US/docs/Web/API/History_API) API.

_If you found this helpful, consider [subscribing to my newsletter](https://upweb.dev) for weekly web development insights and updates. Thanks for reading!_
