The Principles Behind Front-end Routing
Tags: javascript
Published: 2023-10-17 07:05:51

<small>Read on my [blog](https://www.zhangyu.dev/post/128/).</small>

---

Libraries like `react-router` fundamentally depend on a package called `history`.

`history` is essentially a wrapper for the native `window.history` API.

## window.history API

`window.history` provides five methods:

- `go`: Navigate to a specific page; the parameter is a number. `go(1)` goes forward one page, `go(-1)` goes back one page.
- `back`: Equivalent to `go(-1)`.
- `forward`: Equivalent to `go(1)`.
- `pushState`: Adds a new history record.
- `replaceState`: Replaces the current history record.

We mainly use `pushState` and `replaceState`.

### pushState

The `pushState` method takes three arguments.

1. The first argument is the `state` object.
2. The second argument is the title, which is currently unused by the browser. To future-proof your code, it's advisable to pass an empty string here.
3. The third argument is `url`, which is displayed in the browser's address bar in real-time.

The state object can be accessed via `window.history.state` and defaults to `null`.

To demonstrate, open the browser's console on the current page and type `window.history.pushState({state:0},"","/page")`. You'll notice the browser address changes to `/page`.

Run `window.history.state` in the console; you'll see `{state: 0}`.

Run `window.history.back()` to go back a page.

### replaceState

The key difference between `replaceState` and `pushState` is that `replaceState` replaces the current history record.

Open your console and type `window.history.replaceState({state:1},"","/replace")`. You'll notice the browser address changes to `/replace`.

Type `window.history.state` in the console to retrieve the current `{state: 1}`.

Type `window.history.back()` to navigate to the previous page because the last one was replaced by us.

### Tracking History Changes

The browser provides a `popstate` event to listen to history changes. However, this cannot track changes made by `pushState` or `replaceState`, nor can it determine the direction of navigation (forward or backward). It tracks only changes via `go`, `back`, `forward`, and browser navigation buttons.

```javascript
window.addEventListener("popstate", event => {
	console.log(event)
})
```

## A Brief Dive into history's Source Code

The history library solves the limitations of native listeners. It unifies these various APIs into a single `history` object and independently implements listener functionality. When calling `push` or `replace`, it triggers the associated event callback functions and passes in the direction of navigation.

```typescript
// createBrowserHistory

let globalHistory = window.history;

// Call it own listeners in the native popstate event
function handlePop() {
   let nextAction = Action.Pop;
   let [nextIndex, nextLocation] = getIndexAndLocation();
   // Call it own listeners
   applyTx(nextAction);
}

window.addEventListener('popstate', handlePop);

let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();

// Call it own listeners
function applyTx(nextAction: Action, nextLocation: Location) {
   action = nextAction;
   location = nextLocation;
   listeners.call({ action, location });
}

function push(to: To, state?: any) {
   let nextAction = Action.Push;
   let nextLocation = getNextLocation(to, state);

   let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
   globalHistory.pushState(historyState, '', url);
   // Call listeners when push
   applyTx(nextAction);
}
```

You'll notice that it merely creates its own `listeners` array and manually invokes them during `push` and `replace`, thereby addressing the issues of native APIs not triggering these events.

`createHashHistory` is almost identical to `createBrowserHistory`, but it additionally listens for `hashchange` events.

## Implementing React Router from Scratch

Based on these principles, we can already write a simple router.

Below is a straightforward 20-line implementation example.

```jsx
import React, { useLayoutEffect, useState } from "react";
import { createBrowserHistory } from "history";

const historyRef = React.createRef();

const Router = (props) => {
  const { children } = props;
  if (!historyRef.current) {
    historyRef.current = createBrowserHistory();
  }

  const [state, setState] = useState({
    action: historyRef.current.action,
    location: historyRef.current.location,
  });

  useLayoutEffect(() => historyRef.current.listen(setState), []);

  const {
    location: { pathname },
  } = state;

  const routes = React.Children.toArray(children);
  return routes.find((route) => route.props.path === pathname) ?? null;
};

const Route = (props) => props.children;

function App() {
  return (
    <Router>
      <Route path="/">
        <div onClick={() => historyRef.current.push("/page1")}>index</div>
      </Route>
      <Route path="/page1">
        <div onClick={() => historyRef.current.back()}>page1</div>
      </Route>
    </Router>
  );
}
```

In essence, different `pathname` are used to display different elements. However, `react-router` includes more complex conditions and logic. A more detailed analysis of its source code will be published soon.

---

<small>This post was originally published on December 22, 2021, and was written in Chinese. As I'm currently improving my English skills, I decided to translate one of my shorter posts. You can expect more content in English from me in the future.</small>

Thanks for reading!