In the process of implementing a React single-page application, one essential ecosystem to master is the React-Router library. This article will start from scratch and implement the key parts of React-Router.
React-Router v4 is a groundbreaking update that is completely incompatible with previous versions. Compared to the previous configuration-based routing approach, v4 implements routing through individual route components (
Link
,Route
,Redirect
, etc.). I believe this approach truly aligns with React's component-based thinking. If you already know React, learning React-Router is just a matter of learning a few more components. Therefore, our implementation will mainly focus on v4.
Getting Started#
First, let's quickly start a project using create-react-app
and enter the official demo in index.js
. Let's take a look at the App
component that will be rendered:
const App = () => (
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
<hr/>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
)
This includes the two core components of React-Router: Route
and Link
.
Next, let's focus on implementing these two components.
Route#
In the example above, we can see that Route
can take four props: exact
, path
, component
, and render
. Let's start by implementing the basic Route
component:
class Route extends Component {
static propTypes = {
path: PropTypes.string, // path to match
exact: PropTypes.bool, // whether to match exactly
component: PropTypes.func, // component to render if matched
render: PropTypes.func // custom render content
}
render () {
const { path, exact, component, render } = this.props
// check if the route matches
const match = matchPath(window.location.pathname, { path, exact })
// return null if not matched
if (!match) return null
// if a component is provided
if (component) return React.createElement(component, { match })
// if custom render content is provided
if (render) return render({ match })
return null
}
}
The core of the Route
component is to render if there is a match and not render if there is no match (return null
).
Now let's take a look at the implementation of the matching function matchPath
:
const match = (pathname, options) => {
const { path, exact = false } = options
// if no path is provided
if (!path) return { path: null, url: pathname, isExact: true }
// match the url with a regular expression
const match = new RegExp(`^${path}`).exec(pathname)
// return null if not matched
if (!match) return null
// check if it is an exact match
const url = match[0]
const isExact = pathname === url
if (exact && !isExact) return null
return {
path,
url,
isExact
}
}
To ensure compatibility, React-Router uses the pathToRegex library for regular expression matching. Here, we simply use the built-in
RegExp
object in JavaScript to implement it.
So far, we have implemented the matching and rendering logic of the Route
component. Now, how do we trigger a re-render of the Route
component when the route changes?
class Route extends Component {
...
componentWillMount () {
// listen for clicks on the browser's forward/back buttons
window.addEventListener('popstate', this.handlePop)
}
componentWillUnmount () {
window.addEventListener('popstate', this.handlePop)
}
handlePop = () => {
this.forceUpdate()
}
...
}
There are two scenarios for route changes, one of which is when the browser's forward/back buttons are clicked. We listen for this click event in the two lifecycle methods and call the built-in forceUpdate
method to force a UI update.
In React-Router,
setState
,context
, andhistory.listen
are used together to solve this problem.
The other scenario for route changes is when a Link
component is clicked. This leads us to the implementation of the Link
component.
Link#
The core of the Link
component is to update the URL in a declarative manner. It's easy to see that it ultimately renders an a
tag:
class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool
}
// prevent default navigation and call the custom update route method
handleClick = e => {
const { replace, to } = this.props
e.preventDefault()
replace ? historyReplace(to) : historyPush(to)
}
render () {
const { to, children } = this.props
return (
<a href={to} onClick={this.handleClick}>
{children}
</a>
)
}
}
The difference between the custom historyReplace
and historyPush
methods is whether they insert or replace the history stack:
const historyPush = path => window.history.pushState({}, null, path)
const historyReplace = path => window.history.replaceState({}, null, path)
That's how the Link
component is implemented. Now, we can easily raise a question: how do we match the corresponding Route
when a Link
is clicked? Although we listen for events during the mounting and unmounting of the Route
component, it does not take effect when a Link
is clicked.
Therefore, it is necessary to save the Route
component as an instance when it is loaded (regardless of whether it is matched or not).
componentWillMount () {
window.addEventListener('popstate', this.handlePop)
register(this)
}
componentWillUnmount () {
unregister(this)
window.addEventListener('popstate', this.handlePop)
}
The implementation of register
and unregister
is as follows:
let instances = []
const register = comp => instances.push(comp)
const unregister = comp => instances.splice(instances.indexOf(comp), 1)
So, in the historyPush
and historyReplace
methods, we need to iterate through all the Route
instances and force them to update one by one:
const historyPush = path => {
window.history.pushState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}
const historyReplace = path => {
window.history.replaceState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}
Now, let's import the Route
and Link
components into index.js
and start the project. You should see that it runs correctly.
In React-Router,
setState
,context
, andhistory.listen
are used in combination to solve this problem.
Conclusion#
The principles of React Router v4
are worth learning. React can make you a better JavaScript developer, and React-Router can make you a better React developer.
References: