Demystifying React Router: From Native JS Routing to Source Code Deep Dive
This article walks through building a basic front‑end router with vanilla JavaScript, then dissects React Router’s source code—including BrowserRouter, HashRouter, Router, Route, and matchPath—explaining their implementations, the history library, and how they improve routing compared to native approaches.
Recently a teammate shared a talk on React Router source code, which is concise and a good entry point for understanding front‑end routing principles. While preparing the talk I reflected on front‑end routing and wrote this article to share my insights.
The article first implements a basic front‑end router using native JavaScript, then examines React Router’s source code, comparing the two implementations to analyze React Router’s motivations and advantages.
1. Implementing a Basic Front‑End Router
Front‑end routing essentially requires two functions: listening to route changes and matching those changes to render content.
Route example:
1.1 Hash Implementation
<body>
<a href="#/home">Home</a>
<a href="#/user">User</a>
<a href="#/about">About</a>
<div id="view"></div>
</body>
<script>
// onHashChange event callback, match route change and render content
function onHashChange() {
const view = document.getElementById('view');
switch (location.hash) {
case '#/home':
view.innerHTML = 'Home';
break;
case '#/user':
view.innerHTML = 'User';
break;
case '#/about':
view.innerHTML = 'About';
break;
}
}
// bind hashchange event
window.addEventListener('hashchange', onHashChange);
</script>The hash mode is simple: the hashchange event detects changes after the "#" and renders corresponding content.
1.2 History Implementation
<body>
<a href="/home">Home</a>
<a href="/user">User</a>
<a href="/about">About</a>
<div id="view"></div>
</body>
<script>
// Rewrite all <a> click events
const elements = document.querySelectorAll('a[href]');
elements.forEach(el => el.addEventListener('click', e => {
e.preventDefault();
const href = el.getAttribute('href');
history.pushState(null, null, href);
onPopState();
}));
// onpopstate callback, match route change and render content
function onPopState() {
const view = document.querySelector('#view');
switch (location.pathname) {
case '/home':
view.innerHTML = 'Home';
break;
case '/user':
view.innerHTML = 'User';
break;
case '/about':
view.innerHTML = 'About';
break;
}
}
window.addEventListener('popstate', onPopState);
</script>History mode requires intercepting link clicks, using the HTML5 History API ( pushState, replaceState, popstate) to modify the URL without a page refresh.
Tip: Running the history mode code via a local HTTP server (e.g., http-server ) avoids the "file://" origin security error.
2. React Router Overview
React Router provides three router components ( <BrowserRouter>, <HashRouter>, <MemoryRouter>) and several routing primitives ( <Route>, <Switch>, <Link>, <NavLink>). The article first reviews basic usage before diving into the source.
2.1 BrowserRouter and HashRouter
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter; import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default HashRouter;Both components are thin wrappers that create a history object (browser or hash) and pass it to <Router>. The main difference lies in the history implementation they import.
2.2 Router Implementation
import RouterContext from "./RouterContext";
import React from "react";
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = { location: props.history.location };
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
if (this.unlisten) this.unlisten();
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
{this.props.children || null}
</RouterContext.Provider>
);
}
}
export default Router;The Router stores the current location in state, listens to history changes, and provides history, location, and match via context to descendant routing components.
2.3 Route Implementation
import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "../utils/matchPath.js";
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
export default Route;Route consumes the router context, determines a match using matchPath (or a pre‑computed match from <Switch>), and renders either children, a component, or a render prop based on priority.
2.4 matchPath Implementation
import pathToRegexp from "path-to-regexp";
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path,
url: path === "/" && url === "" ? "/" : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
export default matchPath; matchPathcompiles route patterns into regular expressions using path-to-regexp, then tests the current pathname to produce a match object containing path, url, isExact, and extracted params.
3. Putting It All Together
Using React Router, the same routing logic can be expressed declaratively:
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
const App = () => (
<BrowserRouter>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user">User</Link>
<Switch>
<Route path="/about" component={About} />
<Route path="/user" component={User} />
<Route path="/" component={Home} />
</Switch>
</BrowserRouter>
);
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const User = () => <h2>User</h2>;
export default App;Here <Link> replaces the native <a> tag, <Route> replaces the manual onPopState logic, and <BrowserRouter> supplies the history listener, demonstrating how React Router abstracts the low‑level details while preserving the core concepts of listening and matching.
In summary, React Router’s source code is relatively straightforward: it uses the history library to unify route listening across different environments, passes routing data via React context, and employs matchPath (backed by path-to-regexp) to perform flexible pattern matching. Understanding these internals provides valuable insights for designing custom routing solutions or evaluating alternative routers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
