preface

Router is an essential part of current single-page applications. Today, we will use React-Router V4 as an example to explain why router is an essential part of current single-page applications. This article does not focus on the basic concepts of REacr-Router V4; you can visit the official documentation to learn more about the basics

In this paper, RRV4 refers to REacr-Router V4

Preliminary knowledge

RRV4 relies on the history library

Let’s start with a couple of questions

Q1. Why do we sometimes see it written like this

 import {
   Switch,
   Route, Router,
   BrowserRouter, Link
 } from 'react-router-dom';
Copy the code

Or something like this?

import {Switch, Route, Router} from 'react-router';
import {BrowserRouter, Link} from 'react-router-dom';
Copy the code

React-router-dom and react-router-dom

Q2. Why is nesting of div tags supported in v4?

Q3. Route renders components when the current URL matches the value of the path property. How does it do this?

Q4. Why Link component instead of A tag?

routing

Before entering RR V4, let’s think about what routing does, which is to synchronize the URL with its corresponding callback function. Pushstate and replacestate methods are used to modify the URL. Window. addEventListener(‘ popState ‘, callback) is used to listen for forward and back. Hash modifies the hash through window.location.hash and listens for changes through window.addeventListener (‘hashchange’, callback)

The body of the

To understand how this works, let’s start with a simple piece of code for RRV4

   <BrowserRouter>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
      </ul>

      <hr/>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </div>
  </BrowserRouter>
Copy the code

Look at the props for the child component of Route

 {
    match: {
        path: "/".// The path to match
        url: "/".// The current URL
        params: {},  // Parameters in the path
        isExact:true     // If pathName === "/"
    },
    location: {
        hash: "" // hash
        key: "nyi4ea" // Unique key
        pathname: "/" // The path part of the URL
        search: "" / / URL parameters
        state: undefined // The state parameter passed when the route jumps} history: {... }// The history library provides
    staticContext: undefined  // For server-side rendering
    
 }
Copy the code

React-router-dom is based on react-router, router, Route, Switch, etc., and Link, BrowserRouter, etc. The HashRouter component, which explains Q1 here, is react-Router for general route management, react-router-dom for web, and of course, React-router-native for RN management. Let’s start with BrowserRouter

BrowserRouter

Rrv4’s authors advocate the concept of Just Components, BrowserRouter is simple, wrapping the Router as a component, and history is passed along, as is HashRouter

BrowserRouter source

import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";


class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />; }}Copy the code

The Router component of BrowserRouter is the Router component of BrowserRouter.

Router

The Router, as the root component of the Route, is responsible for monitoring URL changes and delivering data (props). In this case, history.listen is used to listen to the URL, and the React Context Provider and Consumer modes are used. Pass history, location, match, and staticContext as props

Router source code + annotations


/ / structure props
function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      // browserRouter props is history
      location: props.history.location
    };
    this._isMounted = false;
    this._pendingLocation = null;

    // Render server side when staticContext is true
    / / staticContext to false
    if(! props.staticContext) {// Listen, location changes triggered
      this.unlisten = props.history.listen(location => {
        // _isMounted is true after didmount and can be setState to prevent setState in constructors
        if (this._isMounted) {
          // Update state location
          this.setState({ location });
        } else {
          // otherwise store to _pendingLocation and wait until didmount to setState to avoid possible errors
          this._pendingLocation = location; }}); } } componentDidMount() {// Assign true and do not change
    this._isMounted = true;
    / / update the location
    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
  // Cancel the listener
    if (this.unlisten) this.unlisten();
  }
  
  render() {
    const context = getContext(this.props, this.state);
    return (
      <RouterContext.Provider
        children={this.props.children || null} value={context} /> ); }}Copy the code

The rrV4 Router component is the Pirover, children, and other elements in the context. This explains the problem Q2. The v4 version of the React-Router directly overrules v2, v3, and v2. In V3, the Router component generates a global routing table based on the Route of the subcomponent. The routing table records the mapping relationship between path and UI components. The Router monitors the change of path. In RRV4, the author advocated the idea of Just Components, which is also in line with the idea of everything Components in React.

Route

In V4, Route is just a consumer-wrapped React component. Regardless of the path, the Route component will always render and determine inside the Route whether the request path matches the current path. Matches continue to render children in Route or component or children in Render, or null if they do not match

Route source code + annotations


function getContext(props, context) {
  const location = props.location || context.location;
  const match = props.computedMatch
    ? props.computedMatch // <Switch> already computed the match for us
    : props.path  // <Route path='/xx' ... >
      ? matchPath(location.pathname, props)
      : context.match; / / the default {path: "/", url: "/", params: {}, isExact: the pathname = = = "/"}

  return { ...context, location, match };
}

class Route extends React.Component {
  render() {
    return( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Route> outside a <Router>"); // This. Props = {exact, path, component, children, render, computedMatch,... others } // context = { history, location, staticContext, match } const props = getContext(this.props, context); // struct props let {children, component, render} = this. Props; If (array.isarray (children) &&children. Length === 0) {children = null; } if (typeof children === "function") {// Statically componentless children = children(props); if (children === undefined) { children = null; } } return ( <RouterContext.Provider value={props}> {children && ! isEmptyChildren(children) // children && React.Children.count > 0 ? Children: props. // match is true, and <Route... >? component ? React. CreateElement (Component, props) // Create the React component, passing props{... context, location, match } : render ? Render (props) // render method: null: null} </ routerContext. Provider> // priority children > component > render); }} </RouterContext.Consumer> ); }}Copy the code

Route is a component. Each Route listens to its own context and performs a re-rendering, providing new props, props. Match is generated by matchPath to determine whether to render component and render. Here we have to look at the very important matchPath method, which determines the path of the current Route to match the URL.

matchPath

The matchPath method relies on the path-to-regexp library, for example, little 🌰

  var pathToRegexp = require('path-to-regexp')
  var keys = []
  var re = pathToRegexp('/foo/:bar', keys)
  // re = /^\/foo\/([^\/]+?) / /? $/i
  // keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  
Copy the code

MatchPath source + notes

/** * Public API for matching a URL pathname to a path. * @param {*} pathname history.location.pathname * @param {*} Options * Default configuration, whether to match exact globally, add/strict to the end, case-sensitive, path 
      
        * /
      
function matchPath(pathname, options = {}) {
  // use <Switch />, options = location.pathname
  if (typeof options === "string") options = { path: options };
  const { path, exact = false, strict = false, sensitive = false } = options;
  // Store path into paths array
  const paths = [].concat(path);
  return paths.reduce((matched, path) = > {
    if (matched) return matched;
    // compilePath uses the path-to-regexp library internally and caches it
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    // Look for path in pathName
    const match = regexp.exec(pathname);
    // Failed to match
    if(! match)return null;
    // Define the path found as the URL
    const [url, ...values] = match;
    Eg: '/' === '/home'
    const isExact = pathname === url;
    // Make sure to find the URL === pathName for precise matching
    if(exact && ! isExact)return null;

    // Return match object
    return {
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) = > {
        memo[key.name] = values[index];
        returnmemo; }, {}}; },null);
}
Copy the code

Switch

I wonder if the magistrates have noticed

This is why the Switch component renders the Route or Redirect, the first child of the location-matching component. ForEach (this.props. Children, child => {… }) method matches the first subcomponent. If the match is successful, add computedMatch props, whose props value is match. This changes the logic of matchPath

Switch source code

    class Switch extends React.Component { render() { ... Omit irrelevant codelet element, match;

          React.Children.forEach(this.props.children, child => {
            // child is react elemnet
            // match if there is no match context.match
            if (match == null && React.isValidElement(child)) {
              element = child;

              // form uses 
      
              const path = child.props.path || child.props.from;

              // Match a matchmatch = path ? matchPath(location.pathname, { ... child.props, path }) : context.match;// Path undefind is the default mactch
                // note: path is undefined, default is '/'}});return match  // Add computedMatch props to match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null; }}...Copy the code

Here we have the basic workflow and source code for RRV4, solved Q3, and finally looked at Link.

Link

The main purpose of the Link component isto allow the user to jump by clicking on an anchor tag. The a tag is not used because it avoids a page refresh every time the user switches routes. Instead, push and replace are used in the histoy library. PreventDefault to preventDefault behavior, push or repalce jump via history.

Link part of the source code + notes

 class Link extends React.Component {... handleClick(event, context) {if (this.props.onClick) this.props.onClick(event);
        if (
          !event.defaultPrevented && // onClick prevented default
          event.button === 0 && // Ignore non-left-click clicks
          (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.! isModifiedEvent(event)// ignore clicks with modifier keys
        ) {
          // Prevent default behavior
          event.preventDefault();
    
          const method = this.props.replace
            ? context.history.replace
            : context.history.push;
    
          method(this.props.to); }... render() { .... return(<a
              {. rest}
              onClick={event= > this.handleClick(event, context)}
              href={href}
              ref={innerRef}
            />
            )
        }
      
      ''''

 }

Copy the code

Here, the end of this article, welcome to see the arrival of the old man.