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
state
object. - 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)