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.
// createBrowserHistorylet globalHistory = window.history;// Call it own listeners in the native popstate eventfunction 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 listenersfunction 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.