I remember how I felt when I first started learning front-end routing. I was young and just beginning to explore SPA. From the very beginning I kept the programming code and the routing code separate. I felt like they were two different things, like half-brothers who didn’t like each other but had to live together.

Over the past few years, I’ve had the privilege of passing on the idea of routing to other developers. Unfortunately, it turns out that most of our brains seem to think in a similar way to mine. I think there are several reasons for this. First, routing is often very complex. For the authors of these libraries, this makes it more complicated to find the right abstraction in the routing. Second, because of this complexity, consumers of routing libraries tend to blindly trust abstractions without really understanding the underlying situation, and in this tutorial we will delve into both of these issues. First, we’ll get a sense of React Router V4 by recreating a simplified version of our own React Router V4, that is, whether RRv4 is a reasonable abstraction.

Here is our application code, which we can use to test when we implement our routing. The full demo can be seen here

const Home = (a)= > (
  <h2>Home</h2>
)

const About = (a)= > (
  <h2>About</h2>
)

const Topic = ({ topicId }) = > (
  <h3>{topicId}</h3>
)

const Topics = ({ match }) = > {
  const items = [
    { name: 'Rendering with React'.slug: 'rendering' },
    { name: 'Components'.slug: 'components' },
    { name: 'Props v. State'.slug: 'props-v-state'},]return (
    <div>
      <h2>Topics</h2>
      <ul>
        {items.map(({ name, slug }) => (
          <li key={name}>
            <Link to={`${match.url}/${slug}`}>{name}</Link>
          </li>
        ))}
      </ul>
      {items.map(({ name, slug }) => (
        <Route key={name} path={`${match.path}/${slug}`} render={() => (
          <Topic topicId={name} />
        )} />
      ))}
      <Route exact path={match.url} render={() => (
        <h3>Please select a topic.</h3>
      )}/>
    </div>
  )
}

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>
)

Copy the code

If you’re not familiar with React Router V4, here’s a basic introduction. Routes render the CORRESPONDING UI when the URL matches the location you specified in the Routes’ path. Links provides a declarative, accessible way to navigate applications. In other words, the Link component allows you to update the URL, and the Route component changes the UI based on this new URL. The focus of this tutorial is not actually to teach THE basics of RRV4, so if the above code isn’t familiar, take a look at the official documentation.

The first thing to note is that we have introduced into our application the two components that the router provides us (Link and Route). One of my favorite things about React Router V4 is that the apis are just components. This means that if you’re already familiar with React, your intuition about the components and how to combine them will continue to apply to your routing code. What’s even more convenient for our use case here is that since we’re already familiar with how to create components, creating our own React Router just needs to do what we’ve already done.


We’ll start by creating the Route component. Before delving into the code, let’s take a look at the API(which requires very handy tools).

In the example above, you’ll notice that you can include three props. Exact, path and Component. This means that the propTypes of the Route component currently look like this,

static propTypes = {
  exact: PropTypes.bool,
  path: PropTypes.string,
  component: PropTypes.func,
}
Copy the code

There are some subtleties here. First, the reason path is not needed is that if no path is given to Route, it will automatically render. Second, the reason the component is not marked Required is that there are actually several different ways to tell the React Router what UI you want to render if the paths match. One method not present in our example above is the render attribute. It looks like this,

<Route path='/settings' render={({ match }) => {
  return <Settings authed={isAuthed} match={match} />}} / >Copy the code

Render allows you to easily inline a function that returns some UI instead of creating a separate component. We’ll also add that to our propTypes,

static propTypes = {
  exact: PropTypes.bool,
  path: PropTypes.string,
  component: PropTypes.func,
  render: PropTypes.func,
}
Copy the code

Now that we know which props a Route receives, let’s discuss its actual functions again. When the URL matches the location you specify in the Path attribute of the Route, the Route renders the corresponding UI. Based on this definition, we know that we will need some functionality to check whether the current URL matches the path attribute of the component. If so, we will render the corresponding UI. If not, we will return null.

Let’s see what this looks like in code, and we’ll implement the matchPath function later.

class Route extends Component {
  static propTypes = {
    exact: PropTypes.bool,
    path: PropTypes.string,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  render () {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(
      location.pathname, // global DOM variable
      { path, exact }
    )

    if(! match) {// Do nothing because the current
      // location doesn't match the path prop.

      return null
    }

    if (component) {
      // The component prop takes precedent over the
      // render method. If the current location matches
      // the path prop, create a new element passing in
      // match as the prop.

      return React.createElement(component, { match })
    }

    if (render) {
      // If there's a match but component
      // was undefined, invoke the render
      // prop passing in match as an argument.

      return render({ match })
    }

    return null}}Copy the code

Now, Route looks pretty stable. If the path passed in is matched, we render the component otherwise null is returned.

Let’s step back and talk about routing. In a client application, there are only two ways for a user to update a URL. The first is by clicking the anchor TAB, and the second is by clicking the Back/forward button. Our router needs to know the current URL and render the UI based on it. This also means that our route needs to know when the URL has changed so that it can use this new URL to decide which new UI to display. If we know that the only way to update urls is through anchor tags or forward/back buttons, then we can start planning and responding to these changes. Later, when we build the component, we’ll discuss anchor tags, but for now, I want to focus on the back/forward buttons. The React Router uses the history. listen method to listen for changes to the current URL, but to avoid introducing other libraries, we’ll use HTML5 popState events. Popstate is exactly what we need, and it fires when the user clicks the forward or back button. Because it is routing that renders the UI based on the current URL, it also makes sense for a yield to be able to listen and re-render when a PopState event occurs. By re-rendering, each route is re-checked to see if it matches the new URL. If there is, they render the UI, if not, they do nothing. Let’s see what this looks like,

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate".this.handlePop)
  }

  componentWillUnmount() {
    removeEventListener("popstate".this.handlePop)
  }

  handlePop = (a)= > {
    this.forceUpdate()
  }

  render() {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(location.pathname, { path, exact })

    if(! match)return null

    if (component)
      return React.createElement(component, { match })

    if (render)
      return render({ match })

    return null}}Copy the code

You should note that all we’re doing is adding a popState listener when the component is mounted, and when the PopState event is triggered, we call forceUpdate, which will start the re-render.

Now, no matter how many we render, they will listen, rematch, and rerender based on the Forward /back button.

Until then, we’ve been using the matchPath function. This function is critical for our routing because it will determine whether the current URL matches the path of the component we discussed above. One of the nuances of matchPath is that we need to make sure we take into account the exact attribute. If you don’t know exactly how to do it, here’s an explanation straight from the documentation,

When true, it matches only if the path is equal to location.pathname.

path location.pathname exact matches?
/one /one/two true no
/one /one/two false yes

Now, let’s dive into the implementation of the matchPath function. If you look back at the Route component, you’ll see that matchPath is called like this,

const match = matchPath(location.pathname, { path, exact })
Copy the code

Whether a match is an object or null depends on whether there is a match. Based on this call, we can build the first part of matchPath,

const matchPath = (pathname, options) => {
  const { exact = false, path } = options
}
Copy the code

Here we use some ES6 syntax. That means, create a variable called exact which is equal to options.exact, or set to false if not defined. Also create a variable named path, which is equal to options.path.

I mentioned earlier that “path is not required because it will automatically render if no path is given “. Since it is indirectly our matchPath function, which determines whether the UI is rendered (by whether there is a match), let’s add this functionality now.

const matchPath = (pathname, options) = > {
  const { exact = false, path } = options

  if(! path) {return {
      path: null.url: pathname,
      isExact: true,}}}Copy the code

Then there’s the matching part. The React Router uses pathToRegexp to match paths. For simplicity we use a simple regular expression here.

const matchPath = (pathname, options) = > {
  const { exact = false, path } = options

  if(! path) {return {
      path: null.url: pathname,
      isExact: true,}}const match = new RegExp(` ^${path}`).exec(pathname)
}
Copy the code

.exec returns an array of matched paths, otherwise null. Let’s look at an example of a path that matches when we route to /topics/ Components.

If you’re not familiar with.exec, it returns an array of matched text if it finds a match, otherwise it returns null.

Here’s each match when our sample application was routed to /topics/ Components

path location.pathname return value
/ /topics/components / ‘/’
/about /topics/components null
/topics /topics/components [‘/topics’]
/topics/rendering /topics/components null
/topics/components /topics/components [‘/topics/components’]
/topics/props-v-state /topics/components null
/topics /topics/components [‘/topics’]

Notice that we are using this for each application<Route>I got a match. That’s because every<Route>Called in its render methodmatchPath.

Now that we know what the match returned by exec is, all we need to do is determine if there is a match.

const matchPath = (pathname, options) = > {
  const { exact = false, path } = options

  if(! path) {return {
      path: null.url: pathname,
      isExact: true,}}const match = new RegExp(` ^${path}`).exec(pathname)

  if(! match) {// There wasn't a match.
    return null
  }

  const url = match[0]
  const isExact = pathname === url

  if(exact && ! isExact) {// There was a match, but it wasn't
    // an exact match as specified by
    // the exact prop.

    return null
  }

  return {
    path,
    url,
    isExact,
  }
}
Copy the code

As I mentioned earlier, if you are a user, there are only two ways to update the URL, either through the back/forward button or by clicking the anchor TAB. Now that we’ve handled the re-rendering of back/forward clicks via popState event listeners in the route, let’s handle the anchor tags by building the component.

The Link API looks like this,

<Link to='/some-path' replace={false} / >Copy the code

To is a string that is the location to link to, while replace is a Boolean value that, when true, replaces the current entry in the history stack rather than adding a new one.

Adding these propTypes to the link component, we get,

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
}
Copy the code

Now we know that the Render method in the Link component needs to return an anchor tag, but we obviously don’t want to cause the entire page to refresh every time we switch routes, so we’ll hijack the anchor tag by adding an onClick handler to it

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }

  handleClick = (event) = > {
    const { replace, to } = this.props
    event.preventDefault()

    // route here.
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>)}}Copy the code

All that is missing is a change of position. To do this, the React Router uses the Push and replace methods of History, but we’ll use HTML5’s pushState and replaceState methods to avoid adding dependencies.

In this article, we willHistoryLibrary as a way to avoid external dependencies, but it is very important for realReact RouterThe code is important because it regulates the differences in managing session history in different browser environments.

Both pushState and replaceState take three parameters. The first is the object associated with the new history entry — we don’t need this functionality, so we just pass an empty object. The second is title, and we don’t need that either, so we pass null. The third one, which we’re going to use, is a relative URL.

const historyPush = (path) = > {
  history.pushState({}, null, path)
}

const historyReplace = (path) = > {
  history.replaceState({}, null, path)
}
Copy the code

Now in our Link component, we’re going to call historyPush or historyReplace depending on the replace property,

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
  handleClick = (event) = > {
    const { replace, to } = this.props
    event.preventDefault()

    replace ? historyReplace(to) : historyPush(to)
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>)}}Copy the code

Now, there’s only one more thing we need to do, and it’s crucial. If you run our sample application with our current router code, you will find a considerable problem. When navigating, the URL will be updated, but the UI will remain exactly the same. This is because even if we use the historyReplace or historyPush functions to change the location, our

doesn’t know about the change and that they should be rerendered and matched. To solve this problem, we need to keep track of which < routes > have been rendered and call forceUpdate when the Route changes.

React RouterThrough the use ofsetState,contextandhistoryTo solve this problem. Listen inside the router component that wraps code.

To keep the router simple, we’ll keep track of which < routes > have been rendered by saving instances of

into an array, and then every time a position change occurs, we can walk through the array and call forceUpdate on all instances.

let instances = []

const register = (comp) = > instances.push(comp)
const unregister = (comp) = > instances.splice(instances.indexOf(comp), 1)
Copy the code

Notice that we created two functions. Whenever

is mounted, we call register; We will call unregister every time

is uninstalled. Then, whenever historyPush or historyReplace is called (which we will whenever the user clicks ), we can iterate over these instances and forceUpdate.

Let’s first update our

component,

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate".this.handlePop)
    register(this)
  }

  componentWillUnmount() {
    unregister(this)
    removeEventListener("popstate".this.handlePop)
  }
  ...
}
Copy the code

Now, let’s update historyPush and historyReplace,

const historyPush = (path) = > {
  history.pushState({}, null, path)
  instances.forEach(instance= > instance.forceUpdate())
}

const historyReplace = (path) = > {
  history.replaceState({}, null, path)
  instances.forEach(instance= > instance.forceUpdate())
}
Copy the code

Now, each

will be aware of this and re-match and re-render every time the is clicked and the location is changed.

Now, our complete router code is shown below, which works perfectly with the sample application above.

import React, { PropTypes, Component } from 'react'

let instances = []

const register = (comp) = > instances.push(comp)
const unregister = (comp) = > instances.splice(instances.indexOf(comp), 1)

const historyPush = (path) = > {
  history.pushState({}, null, path)
  instances.forEach(instance= > instance.forceUpdate())
}

const historyReplace = (path) = > {
  history.replaceState({}, null, path)
  instances.forEach(instance= > instance.forceUpdate())
}

const matchPath = (pathname, options) = > {
  const { exact = false, path } = options

  if(! path) {return {
      path: null.url: pathname,
      isExact: true}}const match = new RegExp(` ^${path}`).exec(pathname)

  if(! match)return null

  const url = match[0]
  const isExact = pathname === url

  if(exact && ! isExact)return null

  return {
    path,
    url,
    isExact,
  }
}

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate".this.handlePop)
    register(this)
  }

  componentWillUnmount() {
    unregister(this)
    removeEventListener("popstate".this.handlePop)
  }

  handlePop = (a)= > {
    this.forceUpdate()
  }

  render() {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(location.pathname, { path, exact })

    if(! match)return null

    if (component)
      return React.createElement(component, { match })

    if (render)
      return render({ match })

    return null}}class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
  handleClick = (event) = > {
    const { replace, to } = this.props

    event.preventDefault()
    replace ? historyReplace(to) : historyPush(to)
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>)}}Copy the code

The React Router also comes with an additional

component. Creating this component is very simple using the code we wrote earlier.

class Redirect extends Component {
  static defaultProps = {
    push: false
  }

  static propTypes = {
    to: PropTypes.string.isRequired,
    push: PropTypes.bool.isRequired,
  }

  componentDidMount() {
    const { to, push } = this.props

    push ? historyPush(to) : historyReplace(to)
  }

  render() {
    return null}}Copy the code

Note that this component doesn’t actually render any UI; instead, it just acts as a routing controller, hence the name.

I hope this helps you create a better mental model of what’s going on inside the React Router, as well as helping you appreciate the elegance of the React Router and the “Just Components” API. I always said React will make you a better JavaScript developer. I also now believe that the React Router will make you a better React developer. Because everything is a component, so if you know React, you know React Router.

Build your own React Router V4

(after)