preface

The React Router has become a necessity during the React stack learning process. It is the complete React routing solution.

In this article, readers are expected to learn not only how to use the React Router, but also how to implement it.

Therefore, the author plans to explain it in the following two sections:

1. React Router Theory: Explain the concepts of the React Router in detail.

Implement a simple Router version step by step.

React Router Theory

What is front-end routing

Change and listen to the URL to make a DOM node display the corresponding view.

Based on the sample

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

export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Topics() {
  let match = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={` ${match.url} /components`} >Components</Link>
        </li>
        <li>
          <Link to={` ${match.url} /props-v-state`} >
            Props v. State
          </Link>
        </li>
      </ul>
      <Switch>
        <Route path={` ${match.path} /:topicId`} >
          <Topic />
        </Route>
        <Route path={match.path}>
          <h3>Please select a topic.</h3>
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let { topicId } = useParams();
  return <h3>Requested topic ID: {topicId}</h3>;
}
Copy the code

This is an example of nested routines, and the end result looks like this:

As the URL changes, the front end route displays the corresponding component and does not trigger the whole page refresh. This is the front end route, do not think of it too complicated.

In this example, we see a lot of new faces: Router, Switch, Route, and so on. The React Router API is used in the React Router.

The router

BrowserRouter

The core of every React Router application should be the Router component. For Web projects, react-router-dom provides BrowserRouter and HashRouter routers.

BrowserRouter uses a regular path, http://baidu.com/path/a1, which is implemented through JavaScript’s history object, which we’ll explain in more detail in The Principles section.

HashRouter

The HashRouter uses a hash path, http://baidu.com/#/path/a1.

Hash = ‘foo’ to change the path from baidu.com to baidu.com/#foo.

The window.addeventListener (‘hashchange’) event is used to listen for changes to the hash value.

Routing matcher

There are two types of routing matching components: Switch and Route. When the Switch component is rendered, its Route child element is searched, which renders the Route matched by the first path found and ignores all other routes.

Route

Define the relative paths of the presentation components and render the corresponding components when the paths match.

<Route path="/contact">
	<Contacts />
</Route>
Copy the code

Switch

Find the Route that matches the first path, and otherwise render all Route components that match the path if there is no Switch.

	<div>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/:user">
      <User />
    </Route>
    <Route>
      <NoMatch />
    </Route>
  </div>
Copy the code

When the path matches /about, all three components match and are shown. But what if you only want to match one component?

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







The Switch component is also similar in concept to the JavaScript Switch function.

navigation

The Link to Redirect

Jump and redirect, this is very simple.

<Link to="/">Home</Link> /login"/> // Redirect to the login sectionCopy the code

Hook

The React Router comes with hooks that let you access the Router state and perform route jumps from within the component.

  • useHistory: gethistoryObject is used to adjust routes. It is equivalent toLinkComponent functions.
  • useLocation: getlocationObject, as long asURLChange, and it returns a new location.
  • useParams: getURLMatches parameter values, such as route definitions/blog/:slugWhen the access/blog/barWhen, throughlet { slug } = useParams();  slugThe value is equal tobar 。

These are some of the core concepts of the React Router, and they’re pretty straightforward. Let’s implement a Router together.

The React principle of the Router

There are usually two ways to implement browser-side routing, hash and the history object provided by H5, which will be used in this article.

history

pushState()

history.pushState(state, title[, url])
Copy the code
  • stateRepresents the state object, which allows us to create its own state for each route;
  • title, has no effect for the time being;
  • url , URLThe path.

Example:

history.pushState({foo:'foo'}, ' ', foo) can change example.com to example.com/foo.Copy the code

A similar method is replaceState(), which is very similar to pushState() except that the former replaces the history and the latter adds the history.

History does not cause the browser to reload

Using location.href = ‘baidu.com/foo’ would cause the browser to reload the page and request the server, but the magic of history.pushState() is that it can change the URL without reloading the page, It is entirely up to the user to decide how to handle this URL change, which is a necessary feature for implementing front-end routing.

After the history route is refreshed, 404 is displayed

The server does not map the url to baidu.com/foo, so it returns 404. The server returns index.html for pages that it does not recognize. The home page, which contains the JavaScript code associated with front-end routing, will load your front-end routing configuration table, and even though the server gives you the home page file, but your URL is baidu.com/foo, the front-end routing will load the view corresponding to the path /foo. Solved 404 problem perfectly.

The history library

The React Router uses github.com/ReactTraini… This library.

Its basic use is as follows:

import { createBrowserHistory } from 'history';
// Create a history object
const history = createBrowserHistory();
// Get the location object
const location = history.location;
// Listen on the history object
const unlisten = history.listen((location, action) = > {
  console.log(action, location.pathname, location.state);
});
// Jumps to the specified address and pushes a new record into the browser session history stack.
history.push('/home', { some: 'state' });
Copy the code

A simple implementation of the Listen method is as follows:

let listeners = [];
function listen(fn) {
  listeners.push(fn);
}
Copy the code

2. Listeners are simple arrays of functions that are pushed into them whenever they are listened on.

A simple implementation of the push method is as follows:

function push(to, state) {
  window.history.pushState(state, ' ', to);
  listeners.forEach(fn= > fn(location));
}
Copy the code

Resolution:

  • The bottom line is through callshistory.pushStateMethods to changeURL ;
  • The subscription function is then executed (essentially a simple publish-subscribe process).

The path – to – the regexp library

Another important tool library used by the React Router is path-to-regexp, which handles url addresses and parameters to get the data you want.

Example:

import { pathToRegexp } from "path-to-regexp";

const regexp = pathToRegexp("/foo/:bar"); // get regexp = /^\/foo\/([^\/]+?) / /? $/iRegexp. Exec (STR);Copy the code

With this knowledge in hand, we’re ready to implement a simple React Router.

imitation-react-router

Let’s review the React Router components:

  • BrowserRouterTo providehistoryObject and passContextDeliver to all descendant components;
  • SwitchTo find the first path matchingRoute ;
  • RoutewhenpathWhen matched, the corresponding component is rendered;
  • LinkIntercept,aThe TAB clicks on the event and passeshistory.pushMethods to changeURL 。

Before writing the React component, we implement two utility methods: Context and matchPath.

Context.js

Initialize a Context.

import React from "react";
const Context = React.createContext(null);

export default Context;
Copy the code

matchPath.js

Determine whether the route matches the path, that is, /topics/: ID matches /topics/components.

import { pathToRegexp } from "path-to-regexp";

export function matchPath(pathname, path) {
  const keys = [];
  const regexp = pathToRegexp(path, keys);/ / {1}
  const match = regexp.exec(pathname); / / {2}

  if(! match)return null;
  const values = match.slice(1);

  return {
    params: keys.reduce((memo, key, index) = > {
      memo[key.name] = values[index];
      returnmemo; }, {}}; }Copy the code

Resolution:

  • {1}, throughpathToRegexpMethod, based on the path passed in for example/topics/:idAnd generate the corresponding matching re/^\/topics(? : \ / ([^ # \ \ /?] +? ) / # \ \ /? ? $/i ;
  • {2}, to perform the regularexecMatch the actual path passed, for example/topics/components ;
  • And finally print oneparamsObject, which contains something like this:
params:{
  "id": "components"
}
Copy the code

BrowserRouter

Create the history object and use the Context to deliver it to all descendant nodes. Let’s see how it is implemented.

import React, { useState, useCallback } from "react";
import { history } from ".. /lib-history/history";
import Context from "./Context";

const BrowserRouter = props= > {
  / / {1}
  const [location, setLocation] = useState(history.location); 
  / / {2}
  const computeRootMatch = useCallback(pathname= > {
    return { path: "/".url: "/".params: {}, isExact: pathname === "/"}; } []);/ / {3}
  history.listen(location= > {
    setLocation(location);
  });
  / / {4}
  return (
    <Context.Provider
      value={{ history.location.match: computeRootMatch(location.pathname)}} >
      {props.children}
    </Context.Provider>
  );
};

export default BrowserRouter;
Copy the code

Resolution:

  • Based on {1}history.locationCreate state;
  • {2}, to initialize the root pathmatchObject;
  • {3}, listeninghistoryObject that is executed when it has changedlocationState to refresh all components;
  • {4}, through the above creationContext,History, location, matchObject asvalueValue.

Route

There are two uses, one is included with the Switch, and one is used independently.

import React, { useContext } from "react";
import Context from "./Context";
import { matchPath } from "./matchPath";

const Route = props= > {
  / / {1}
  const { location, history } = useContext(Context);
  / / {2}
  const match = props.computedMatch
    ? props.computedMatch
    : matchPath(location.pathname, props.path);

  return (
    <Context.Provider value={{... match,location,history}}>
      {/* 3 */}
      {match ? props.children : null}
    </Context.Provider>
  );
};

export default Route;
Copy the code

Resolution:

  • {1} to useuseContext HookTo obtaincontextThe values;
  • {2},props.computedMatchIs included in theSwtichIs passed in, so it checks whether the property exists and if so, it is usedSwitchincomingmatchObject, if noneprops.computedMatchSaid,RouteIf it is used independently, use itmatchPathTo do the matching, get the matching results;
  • {3}, according to the matching results to determine whether to render the component.

Link

The main purpose of this component is to intercept the click event of the A tag.

import React, { useContext, useCallback } from "react";
import Context from "./Context";

const Link = ({ to, children }) = > {
  const { history } = useContext(Context);
  
  const handleOnClick = useCallback(
    event= > {
      event.preventDefault();
      history.push(to);
    },
    [history, to]
  );

  return (
    <a href={to} onClick={handleOnClick}>
      {children}
    </a>
  );
};

export default Link;
Copy the code

Switch

Find the Route that matches the first path.

import React, { useContext } from "react";
import Context from "./Context";
import { matchPath } from "./matchPath";

const Switch = props= > {
  const context = useContext(Context);

  const location = context.location;

  let element,
    match = null;
  / / {1}
  React.Children.forEach(props.children, child= > {
    if (match === null && React.isValidElement(child)) {
      element = child;
	  / / {2}
      const path = child.props.path;
	  / / {3}match = path ? matchPath(location.pathname, child.props.path) : context.match; }});/ / {4}
  return match
    ? React.cloneElement(element, { location, computedMatch: match })
    : null;
};

export default Switch;
Copy the code

Resolution:

  • {1}, throughReact.ChildrenAccess to theSwitchAll descendant elements under the component and iterate through them;
  • {2}, access toRoute 的 pathThe attribute value;
  • {3}, by judgmentpathIf yes, the path is matchedmatchPath, is used if there is no valueBrowserRouterIncoming to proceedmatchObject, which means if<Route><Foo /></Route>If no path is set, the default root path will be matched.
  • {4}, if a match is matched, passReact.cloneElementMethod returns a new clone element and sets the{ location, computedMatch: match }As apropsThe incoming.

Hooks

import React from "react";
import Context from "./Context";

export function useParams() {
  return React.useContext(Context).params;
}

export function useHistory() {
  return React.useContext(Context).history;
}

export function useLocation() {
  return React.useContext(Context).location;
}
Copy the code

Hook implementation is relatively simple, mainly through the Context to get the corresponding object, back out.

The overall implementation does not consider too many boundary conditions, mainly in order to quickly understand the Router principle. Finally, a simple example:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams,
} from "./imitation-react-router";

export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
          <li>
            <Link to={` /topics/components`} >Components</Link>
          </li>
        </ul>
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path={` /topics/:id`} >
            <Topic />
          </Route>
          <Route path="/home">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Topic() {
  let { id } = useParams();
  return <h3>Requested topic ID: {id}</h3>;
}
Copy the code

A simple case can run successfully.

conclusion

The React Router itself is not complicated, but you can learn a lot about it by learning its source code. If you compare the implementation of Redux, you’ll see that the global object is delivered by Context. Both use publish subscriptions to notify components of updates.

Click to view the >>> code hosting address

Please give a like if you like this article!