How React Router Work 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);
}
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 callback 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;
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, consider subscribing to my newsletter for weekly web development insights and updates. Thanks for reading!
Ditch Git Checkout - Use Git Switch and Git Restore Instead
Replace git checkout with modern git switch and git restore commands. Learn the differences, benefits, and practical examples for better Git workflow management.
7 Lesser-Known HTML Attributes to Enhance User Experience
Discover 7 hidden HTML attributes that improve web usability: enterkeyhint, ordered list attributes, decoding, loading, crossorigin, disablepictureinpicture, and integrity for better user experience.