Read on my blog.
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 togo(-1). - 
forward: Equivalent togo(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.
- The first argument is the 
stateobject. - 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.
 - 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.
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.
// 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.
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.
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.
Thanks for reading!
    
Top comments (0)