React -router-dom use and source code implementation

React-router-dom is a routing solution specifically for Web applications.

Router, Link, Route

1.1. Basic use

Let’s start with a simple example:

import React from "react";
import {
  BrowserRouter as Router,
  Route,
  Link
} from "react-router-dom";

export default function App() {
  return (
    <div className="App">
      <Router>
        <Link to="/">Home page</Link>
        <Link to="/user">The user center</Link>
        <Link to="/login">The login</Link>
        <Link to="/product/123">goods</Link>

        <Route
          path="/"
          children={()= > HomePage({ data: "children" })}
          component={HomePage}
          render={() => <HomePage data="render" />} / ><Route path="/user" component={UserPage} />
        <Route path="/login" component={LoginPage} />
      </Router>
    </div>
  );
}

function HomePage(props) {
  console.log("index", props);
  return <div>Home - {props. Data}</div>;
}

function UserPage(props) {
  return <div>The user center</div>;
}

function LoginPage(props) {
  return <div>The login</div>;
}
Copy the code

The example uses the three components of React-router-DOM, BrowserRouter, Route, and Link.

A Router component is like a Router. Link refers to the network cable interface and network cable of the Router, and Route refers to various devices connected to the Router.

With routers, network cables and devices make sense, so the Route and Link components must be placed in the Router component.

The Link component’s to property specifies the Route to jump to when the Link is clicked, the Route component’s Path property specifies the Route that the Route component matches, If matched, render the component specified by the children, Component, and render attributes (all of which specify the component to render). The differences are as follows:

children

The specified component is rendered even if the route does not match

component

The specified component will only be rendered if the route matches. It is best not to use inline functions as this will result in a new component being generated and mounted on each re-rendering rather than updating on the previous component. If you want to use the form of an inline function, it is better to use render or children

render

Suitable for passing inline functions that are executed when a route is matched

Children > Component > render

1.2, source code implementation

To implement a routing-related component, we need to use a library history. This library provides a method createBrowserHistory that can be used to create a history object, store information such as route history, and listen for route changes.

1.2.1 Link component

Start with the simplest, that is the Link component, the essence of a Link component is an A tag

<Link to="/"> home page < / Link ><Link to="/user">The user center</Link>
<Link to="/login">The login</Link>
<Link to="/product/123">goods</Link>
Copy the code

import { useContext } from "react";
import RouterContext from "./RouterContext";

export default function Link(props) {
  const { to, children } = props;
  const context = useContext(RouterContext);

  function onClick(e) {
    // Prevent default behavior
    e.preventDefault();
    // Use the history object in the context passed by the Router component to change the route
    context.history.push(to);
  }
  return (
    <a href={to} onClick={onClick}>
      {children}
    </a>
  );
}
Copy the code

The to attribute of the Link component is the href attribute of a tag, but the default behavior of clicking a tag will refresh the page, while our SPA application will not refresh the page when jumping to the route, so we need to add onClick method to a tag to prevent the default behavior. Then use the push method of the history object (where the history object is passed through the context). The context is passed from the Router component, which is why components such as Link and Route must be written to the Router component.

1.2.2 the Router components

Let’s look at the implementation of the Router component

import React, { useState, useEffect } from "react";
import RouterContext from "./RouterContext";

export default function Router(props) {
  const { history } = props;
  const [state, setState] = useState({ location: history.location });

  function computeRootMatch(pathname) {
    return { path: "/".url: "/".params: {}, isExact: pathname === "/" };
  }

  useEffect(() = > {
    const unlisten = history.listen(({ location }) = > {
      console.log("location", location);
      setState({ location });
    });
    return () = > {
      unlisten();
    };
  });

  const routeObj = {
    history: history,
    location: state.location,
    match: computeRootMatch(state.location.pathname)
  };

  return (
    <RouterContext.Provider value={routeObj}>
      {props.children}
    </RouterContext.Provider>
  );
}
Copy the code

The Router component has a history object in props, which is used to get the location information of the route and to listen for route changes.

The Router component also uses the Context to pass routing information such as history and location to the underlying components, so we need to create a Context object

import React from "react";

const RouterContext = React.createContext();

export default RouterContext;
Copy the code

When the Link component is clicked, the history.push method is called to add the Route address to the history object. Then the History object in the Route component can listen for Route changes and update the Route with setState. The Route component of the Router component also triggers an update. The Route component takes the latest Route address and matches it with its path property. If the match is successful, render the component specified by the Route component or render property. So the Route component implementation ideas are also generally available.

Before implementing the Route component, let’s look at the history on the Router component props.

There are several different types of router components in the React-router-DOM, such as BrowserRouter and HashRouter

Therefore, it can be inferred that these types of components are at the top of the Router component that pass the different types of history objects to the Router component.

BrowserRouter component implementation

import React from "react";
import { createBrowserHistory } from "history";
import Router from "./Router";

export default function BrowserRouter(props) {
  const history = createBrowserHistory();
  return <Router history={history}>{props.children}</Router>;
}
Copy the code
1.2.3 the Route component

Now you can implement the most critical Route component

import React, { useContext } from "react";
import { matchPath } from "react-router-dom";
import RouterContext from "./RouterContext";

export default function Route(props) {
  const context = useContext(RouterContext);
  const { path, children, component, render, computedMatch } = props;
  const { location } = context;

  const match = computedMatch
    ? computedMatch
    : path
    ? matchPath(location.pathname, props)
    : context.match; // location.pathname === path;

  constrouteProps = { ... context, match };// 1. Match the children Component with the priority of render
  // 2. Mismatch: call if children is function, render null otherwise
  let result = null;
  if (match) {
    if (children) {
      result = typeof children === "function" ? children(routeProps) : null;
    } else if (component) {
      result = React.createElement(component, routeProps);
    } else if(render) { result = render(routeProps); }}else {
    result = typeof children === "function" ? children(routeProps) : null;
  }

  return (
    <RouterContext.Provider value={routeProps}>{result}</RouterContext.Provider>
  );
}
Copy the code

We need to get the properties path, children, Component, and render on the Route component via props. Then we need to get the current location object from the context, which holds the current Route address. Then take the path and the current location information to match. Here we use a method matchPath provided by react-router-dom for convenience to match the route and return the match information

const match = matchPath('/users/123', {
  path: '/users/:id'.exact: true.strict: false
})

// Return the match object in the following format:
{
  path: '/users/:id'.url: '/user/123'.isExact: true.params: {
    id: '123'}}Copy the code

Get the match object and render the components in the order children > Component > render.

This completes the basic react-router-dom.

To sum up:

1. The Router component passes the history object through the Context to the Link and Route components.

2. Click Link component to trigger update of history object;

3. The Route component listens for changes in the History object, compares the latest routing information with its own path, and if a match is found renders the component specified by the Component or Render property.

2, the Switch

2.1. Basic use

Placing the Route component in the Switch component renders only the first Route component that matches the Route address.

If it’s just a bunch of Route components, the about, User, and NoMatch components will all be rendered when the Route address is /about.

If wrapped with Switch, only the About component will be rendered.

<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={NoMatch}/>
</Switch>
Copy the code

2.2 source code implementation

You can guess how the Switch might be implemented: walk through the Switch’s child components, find the first one that matches the current routing address, and render it.

import React, { useContext } from "react";
import { matchPath } from "react-router-dom";
import RouterContext from "./RouterContext";

export default function Switch(props) {
  const { location, match } = useContext(RouterContext);
  let computedMatch = null; // Flags match
  let matchEle = null; // Marks the matched element

  // Use react.children. ForEach to iterate over the Switch's Children
  React.Children.forEach(props.children, (child) = > {
    if (!computedMatch && React.isValidElement(child)) {
      matchEle = child;
      computedMatch = child.props.path
        ? matchPath(location.pathname, child.props)
        : match;
    }
  });
  
  let result = match ? React.cloneElement(matchEle, { computedMatch }) : null;

  return result;
}
Copy the code

At the heart of the code above is the use of the react.children. ForEach method to traverse the Switch’s Children.

Because there are three possibilities for the props. Children value:

  1. There is no child node, is undefined
  2. A child node is an object
  3. Multiple child nodes, an array of objects

React provides this method to make it easy to iterate through child components. Once the first matching child component is found, clone it using react. cloneElement and pass the matching information (the match object returned by the matchPath method) to the component.

3, Redirect

3.1. Basic use

Render

to navigate to a new address. This new address overrides the current address in the history stack, similar to a server-side (HTTP 3XX) redirection.

<Link to="/user"> User center </Link><Link to="/login">The login</Link>
<Link to="/product/123">goods</Link>
<Link to="/redirect">redirect</Link>

<Route path="/user" component={UserPage} />
<Route
   path="/redirect"
   render={()= > <Redirect to="/user"></Redirect>} / >
Copy the code

When a link Redirect is clicked, the Route component is rendered, which actually renders the Redirect component and then navigates to a new address, /user, rendering the UserPage component.

3.2 source code implementation

import { useContext, useEffect } from "react";
import RouterContext from "./RouterContext";

export default function Redirect(props) {
  const context = useContext(RouterContext);
  const { history } = context;
  const { to } = props;

  return <LifeCycle onMount={()= > history.push(to)}></LifeCycle>;
}

function LifeCycle(props) {
  useEffect(() = > {
    props.onMount();
  });

  return null;
}
Copy the code

How do class components and function components get the history object

When we implemented the Route component earlier, we passed the context object to the component to be rendered, namely the history, location, and match objects.

  if (match) {
    if (children) {
      console.log("children", children);
      result = typeof children === "function" ? children(routeProps) : null;
      console.log("result", result);
    } else if (component) {
      result = React.createElement(component, routeProps);
    } else if(render) { result = render(routeProps); }}else {
    result = typeof children === "function" ? children(routeProps) : null;
  }
Copy the code

So we can get these three properties in the props of the rendered component, such as this.props. Location

But if we render a component with a child component, we can’t get these three objects in the child component, and we can’t use history.push for route jump and location.search for route parameters, unless the parent component passes these three attributes to the child component. But it’s too cumbersome to pass the child component every time. So react-router-dom gives us some methods to get the history route object directly:

4.1. Basic use
  1. Class component, we can wrap our component using the withRouter high-order component, and then we can get objects like history in props.

    class Detail extends Component {
      constructor(props) {
        super(props)
        console.log('Detail', props)
      }
      render() {
        return <div>Details page</div>
      }
    }
    Detail = withRouter(Detail)
    Copy the code

    If the withRouter component is not wrapped, the Detail component props has no properties such as history

  1. UseHistory, useLocation, useRouteMatch, useParams

    function DetailFunc(props) {
      const history = useHistory()
      const location = useLocation()
      const match = useRouteMatch()
      const params = useParams()
      console.log('DetailFunc routeProps', { history, location, match, params })
      console.log('DetailFunc', props)
      return <div>Functional component Detail</div>
    }
    Copy the code

4.2 source code implementation

1, withRouter

The essence is a function that takes a component as an argument and returns a new functional component. All it does is get the Context object and pass it to the input component.

import RouterContext from './RouterContext'

const withRouter = (WrappedComponent) = > (props) = > {
  return (
    <RouterContext.Consumer>
      {(context) => {
        return <WrappedComponent {. props} {. context} / >
      }}
    </RouterContext.Consumer>)}export default withRouter
Copy the code

2, hooks,

Hooks are simpler to implement by using useContext to retrieve the context and return the history, location, match, etc.

// useHistory,
// useLocation,
// useRouteMatch,
// useParams,

import { useContext } from 'react'
import RouterContext from './RouterContext'

export function useHistory() {
  return useContext(RouterContext).history
}

export function useLocation() {
  return useContext(RouterContext).location
}

export function useRouteMatch() {
  return useContext(RouterContext).match
}

export function useParams() {
  const match = useContext(RouterContext).match
  return match ? match.params : {}
}
Copy the code

At this point, the basic functions of react-router-dom are complete.

Demo code: my-router-dom