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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Demystifying React Router: From Native JS Routing to Source Code Deep Dive

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

compiles 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptReActsource code analysisHistory APIReact Routerfrontend routingHash Routing
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.