1. Principle Analysis

Using the history library to implement listening routes, internal support hash and Bowser route changes.

When using react-router-dom, BrowserRouter or HashRouter is the first choice, corresponding to hash and browser routing rules. The source code of the two components is basically the same. The difference is that the two methods of the history library are called to create the history object. The React.Context is then used to share the history object with the consumer. A function to update the current component is registered in the consumer.

The differences in the source code are as follows

// BrowserRouter
import { createBrowserHistory as createHistory } from 'history';

// HashRouter
import { createHashHistory as createHistory } from 'history';
Copy the code

So let’s just pick one of these, BrowserRouter

2. Explore the details of the source code

2.1. BrowserRouter

  • role

    • createhistoryInstance,
    • willchildrenandhistoryIncoming components<Router/>And return
  • The core source

    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

    You can see that the history object is actually created by createBrowserHistory. The core logic is all in the
    component, which receives the specific history object, The history object currently includes hash, Browser, and memory types.

2.2. The Router

  • role

    • Listen for route changes and store tostateIn the
    • throughContextPass downhistory,location,matchEtc.
  • The core source

    class Router extends React.Component {
      // Default matched route
      static computeRootMatch(pathname) {
        return { path: '/'.url: '/'.params: {}, isExact: pathname === '/' };
      }
    
      constructor(props) {
        super(props);
    
        this.state = {
          location: props.history.location,
        };
    
        this._isMounted = false;
        this._pendingLocation = null;
    
        // Listen for route changes
        if(! props.staticContext) {this.unlisten = props.history.listen(location= > {
            if (this._isMounted) {
              this.setState({ location });
            } else {
              this._pendingLocation = location; }}); }}componentDidMount() {
        this._isMounted = true;
    
        if (this._pendingLocation) {
          this.setState({ location: this._pendingLocation }); }}componentWillUnmount() {
        // Cancel the listener
        if (this.unlisten) {
          this.unlisten();
          this._isMounted = false;
          this._pendingLocation = null; }}render() {
        return (
          <RouterContext.Provider
            value={{
              history: this.props.history.location: this.state.location.match: Router.computeRootMatch(this.state.location.pathname),
              staticContext: this.props.staticContext,}} >
            <HistoryContext.Provider
              children={this.props.children || null}
              value={this.props.history}
            />
          </RouterContext.Provider>); }}Copy the code

    The
    method declares the default matching rule, computeRootMatch. By default, the match succeeds. Remember isExact.

    Listen for route changes in the constructor, using history as an instance created in BrowserRouter through the createBrowserHistory library.

    Note that _isMounted and _pendingLocation are used to handle lazy components. When the route matches, the lazy components are not yet mounted, so _pendingLocation is stored temporarily, and then updated to state in componentDidMount.

2.3. The Route

  • role

    • throughmatchPathDetermine the current<Route/>thepathmatch
    • createProvider, with the presentpathWhether it matches andContextThe rest of the content is shared with the child components
  • The core source

    class Route extends React.Component {
      render() {
        return (
          <RouterContext.Consumer>{context => { const location = this.props.location || context.location; // If no path is passed in, match with matchPath const match = this.props.computedMatch<Switch/>Prop: this.props. Path? matchPath(location.pathname, this.props) : context.match; const props = { ... context, location, match }; let { children, component, render } = this.props; // Children must be a single root if (array.isarray (children) &&isemptyChildren (children)) {children = null; } return (<RouterContext.Provider value={props}>
                  {props.match
                    ? children
                      ? typeof children === 'function'
                        ? __DEV__
                          ? evalChildrenDev(children, props, this.props.path)
                          : children(props)
                        : children
                      : component
                      ? React.createElement(component, props)
                      : render
                      ? render(props)
                      : null
                    : typeof children === 'function'
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : null}
                </RouterContext.Provider>
              );
            }}
          </RouterContext.Consumer>); }}Copy the code
    1. Match:

      • Priority to comparecomputedMatch, this is<Route/>Components are<Switch/>Injected when the component is wrappedpropIf so, use it directly.
      • There is nocomputedMatchTo comparepath, if anypaththroughmatchPathTo deal with
      • There is nopathDirectly usematch, this is<Router/>throughproviderThe passedDefault matching rules, there will beThe match is successful, so404 componentsDon’t writepath
    2. The result of the component:

      • matchWhether the match is successful
        • matching
          • Whether children exist
            • There are
              • Children are functions: execute
              • Either function or component: render
            • There is no
              • Does component exist?
                • Exists: createElement(Component, props)
                • There is no:
                  • Whether render exists
                    • Presence: execution
                    • None: null
        • Don’t match
          • The children is the function
            • Match: execute
            • Mismatch: NULL

      Children >component>render;

      It’s important to note that Components are created with React. CreateElement. React selects whether or not to reuse components based on whether or not the type changes, so it’s best not to write inline functions for component properties to avoid unnecessary overhead

      The final return of the Provider is for subsequent use of the higher-level component withRouter.

2.4. The Switch

Also known as exclusive routing

  • role

    • Only returnThe match is successfulThe child components<Route/>
  • The core source

    class Switch extends React.Component {
      render() {
        return (
          <RouterContext.Consumer>
            {context => {
              invariant(context, 'You should not use <Switch> outside a <Router>');
    
              const location = this.props.location || context.location;
    
              let element, match;
    
              // We use React.Children.forEach instead of React.Children.toArray().find()
              // here because toArray adds keys to all child elements and we do not want
              // to trigger an unmount/remount for two <Route>s that render the same
              // component at different URLs.
              React.Children.forEach(this.props.children, child => {
                if (match == null && React.isValidElement(child)) {
                  element = child;
    
                  const path = child.props.path || child.props.from;
    
                  match = path
                    ? matchPath(location.pathname, { ...child.props, path })
                    : context.match;
                }
              });
    
              return match ? React.cloneElement(element, { location, computedMatch: match }) : null;
            }}
          </RouterContext.Consumer>
        );
      }
    }
    Copy the code

    Why not use react.children.toarray ().find()? Because toArray adds keys to all child elements, it is possible for two different urls to display the same
    unmounted or remounted.

    If match is true, use the component that successfully matched, and add location and computedMatch to it. The
    component uses this computedMatch preferentially when matching. If there is no computedMatch, it uses its own matching method.
    returns the last component through the matching algorithm, so there is only one
    that ends up being rendered on the page.

    The matching algorithm iterates through all the child nodes (i.e.
    ), matches the path of each component, saves it in the variable Element if the match is successful, and iterates until the last child component.

    I have to mention two conditions for the 404 component,

    1. Don’t writepath.
    2. On the<Switch/>The last child component of

    If the Route component does not have a path, it will use the match provided by the Router component. By default, match matches the Router component.
    components are matched through a loop, iterating until the last one. If placed first, even if the 404 component is matched, the next loop will overwrite it and render a null, so it must be the last child of the
    component to ensure that it is matched.

2.5. withRouter

  • role

    • This is aHigh order componentTo remove<Route/>throughProviderShared data. HereThe target componentuse
  • The core source

    function withRouter(Component) {
      const C = props= > {
        const{ wrappedComponentRef, ... remainingProps } = props;return (
          <RouterContext.Consumer>
            {context => {
              return <Component {. remainingProps} {. context} ref={wrappedComponentRef} />;
            }}
          </RouterContext.Consumer>
        );
      };
    
      return hoistStatics(C, Component);
    }
    Copy the code

    Remember that the
    component finally returns a provider that shares the path match for the component to use via withRouter

2.6. The Link

Link is essentially the Consumer of the Context, fetching history and changing the route by doing history.replace or history.push

2.7. Redirect

  • role

    • redirect
  • The core source

    function Redirect({ computedMatch, to, push = false }) {
      return (
        <RouterContext.Consumer>{context => { const { history, staticContext } = context; const method = push ? history.push : history.replace; const location = createLocation( computedMatch ? typeof to === 'string' ? generatePath(to, computedMatch.params) : { ... to, pathname: generatePath(to.pathname, computedMatch.params), } : to ); return (<Lifecycle
                onMount={()= >{ method(location); }} onUpdate={(self, prevProps) => { const prevLocation = createLocation(prevProps.to); if ( ! locationsAreEqual(prevLocation, { ... location, key: prevLocation.key, }) ) { method(location); } }} to={to} /> ); }}</RouterContext.Consumer>
      );
    }
    Copy the code

    Both Redirect and Route are used as child components of Switch, except that the Redirect component does not render entities, just for redirection

    Finally Lifecycle component is returned, which is a good lesson to learn and can be used in our project scenarios to implement more complex logic

    The source code is as follows:

    class Lifecycle extends React.Component {
      componentDidMount() {
        if (this.props.onMount) this.props.onMount.call(this.this);
      }
    
      componentDidUpdate(prevProps) {
        if (this.props.onUpdate) this.props.onUpdate.call(this.this, prevProps);
      }
    
      componentWillUnmount() {
        if (this.props.onUnmount) this.props.onUnmount.call(this.this);
      }
    
      render() {
        return null; }}Copy the code

    As you can see, the render method returns null at the end, indicating that the actual DOM is not being rendered, but something is being done with the React component’s lifecycle,

    So the Redirect component just ends up doing history.push or history.replace when it’s mounted,

    Redirection in react-router-dom comes in two ways

    1. Tabbed redirection:<Redirect to="/home">
    2. Programmatic redirection:this.props.history.push('/home')

2.8. Prompt

With that in mind, Lifecycle component will be used again

  • role

    • Do a blocking query before the page leaves
  • The core source

    function Prompt({ message, when = true }) {
      return( <RouterContext.Consumer> {context => { invariant(context, 'You should not use <Prompt> outside a <Router>'); if (! when || context.staticContext) return null; // Call the platform's query method with history. By default, window.confirm const method = context.history.block; return ( <Lifecycle onMount={self => { self.release = method(message); }} onUpdate={(self, prevProps) => { if (prevProps.message ! == message) { self.release(); self.release = method(message); }}} onUnmount={self => {// call self.release(); }} message={message} /> ); }} </RouterContext.Consumer> ); }Copy the code

    The specific interception method is context.history.block, which is the platform-specific method that is received when creating the Router, and is window.confirm by default because of the current Web environment

2.9. BrowserRouter vs. HashRouter

There was a problem with vue-router, which was caused by the difference between Hash and Browser

  1. A HashRouter is a simple route that does not need to be accessed by a server. BrowserRouter requires the server to parse the URL to return the page, so using BrowserRouter requires configuring address mapping on the back end.
  2. BrowserRouterThe essence of triggering route changes is usingHTML5 history API(pushState,replaceStatepopstateEvents)
  3. HashRouterDoes not supportlocation.keylocation.state.Dynamic routingNeed to pass through?Pass parameters.
  4. Hash History requires only a server to configure an address to go online, but it is rarely used by online Web applications.

2.10. MemoryRouter

The URL history records are saved in memory, do not read, do not write address bar. Can be used in non-browser environments, such as React Native.

3. Think and summarize

  1. LifeCycle components

    About componentization, looking at the Vue source code to my inspiration is the essence of componentization is the virtual dom, but saw the react – found after the router source components also can be used only as a life cycle, and it is indeed a good logical way, some in the component class relate only to the life cycle of treatment may try to achieve through this way, Such as websocket data reception or specific data processing presentation.

  2. cross-platform

    React is the first cross-platform framework that I saw as a student who is new to front-end design. The same is true of react-Router. First, the core of the source code is placed in react-Router. At present, two libraries, react-router-DOM and react-router-native, are derived. The core processing logic of routes is in another library, History. React-router only focuses on how routes are used in React. The specific platform scheme is entrusted to react-router-DOM and React-router-native. This low-coupling method of code is also worth learning.

    Ps: When will React-Native release 1.0?

  3. Render priority in

    children>component>render

    All three can receive the same [route props], including match, location, and history, but children’s match is null when there is no match compared to the other two cases.

  4. About the children

    Sometimes, when we need to render something regardless of whether the current route matches, we can use children. The arguments are exactly the same as render.

  5. About the component

    If we write component, React creates the component through createElement, so avoid using inline functions to avoid unnecessary overhead