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