preface

The react-Router v6 dependency library History is introduced in the react-Router V6 complete Guide to History. The react-Router v6 dependency library History is introduced in the react-Router V6 complete guide to History. It is the core library for the react-Router internal route navigation. In other words, the React-Router only provides a layer of react-based encapsulation around the library.

React-router V6 is a new version of router V6.

At the time of writing this article, the latest version of the React-Router is V6.2.1, so the whole article will analyze this time node. If there are some major updates in the future, we will consider adding the analysis of these features.

This article only covers the React-Router V6 version and does not compare to v5. To learn how to migrate from V5 to V6, see the official migration guide

There are actually three packages in the React-Router repository: React-router, react-router-dom, and react-router-native, where the react-Router package is at the heart of the whole React-Router, and where almost all the methods, components, and hooks that are independent of the runtime platform are defined.

Start with the Router

Routerinreact-routerInternal is mainly used to provide global routing navigation objects (generally byhistoryLibrary) and the current routing navigation state, which is generally required and unique when used in a project.

But we don’t usually use this component directly, BrowserRouter (React-Router-DOM package import), HashRouter (React-Router-dom package import), and MemoryRouter (React-Router package import) that have encapsulated route navigation objects are more likely to be used.

import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

const rootElement = document.getElementById("root");
render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  rootElement
);
Copy the code

Router source code

First we need to define two contexts that store the global route navigation object and the Context of the navigation location.

import React from 'react'
import type {
  History,
  Location,
} from "history";
import {
  Action as NavigationType,
} from "history";

// Contains only the History objects of the go, push, replace, and createHref methods, which are used to jump routes in the React-router
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;

interface NavigationContextObject {
  basename: string;
  navigator: Navigator;
  static: boolean;
}

/** * contains the global context of the navigator object
const NavigationContext = React.createContext<NavigationContextObject>(null!). ;interface LocationContextObject {
  location: Location;
  navigationType: NavigationType;
}
/ * * * contains the current location and the type of action, are commonly used in internal access to current location, the official don't recommend to directly use * / out
const LocationContext = React.createContext<LocationContextObject>(null!). ;// This is the official export of the above two contexts. As you can see, both contexts are defined as unsafe and subject to major changes. Use of these contexts is strongly discouraged
/ * *@internal * /
export {
  NavigationContext as UNSAFE_NavigationContext,
  LocationContext as UNSAFE_LocationContext,
};
Copy the code

In addition, the React-Router provides three hooks based on LocationContext: useInRouterContext, useNavigationType, and useLocation.

/** * assert method */
function invariant(cond: any, message: string) :asserts cond {
  if(! cond)throw new Error(message);
}

/** * Determine whether the current component is in a Router */
export function useInRouterContext() :boolean {
  returnReact.useContext(LocationContext) ! =null;
}
/** * Gets the current jump action type */
export function useNavigationType() :NavigationType {
  return React.useContext(LocationContext).navigationType;
}
/** * get the current jump location */
export function useLocation() :Location {
  // useLocation must be used in the context provided by the Router
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useLocation() may be used only in the context of a <Router> component.`
  );

  return React.useContext(LocationContext).location;
}
Copy the code

Then we define the Router component and use the above two contexts in the Router component:

// In addition, the parsePath method is introduced from history
import {
  parsePath
} from "history";

export interface RouterProps {
  // Route prefixbasename? :string; children? : React.ReactNode;// Required, current location
  /* interface Location { pathname: string; search: string; hash: string; state: any; key: string; } * /
  location: Partial<Location> | string;
  // There are three types of route hop: POP, PUSH and REPLACEnavigationType? : NavigationType;// Must pass, the navigation object in history, where we can pass unified external history
  navigator: Navigator;
  // Static route (SSR)
  static? :boolean;
}

/** * Provides the context for rendering the Route, but this component is usually not used directly, There should be only one Router in the entire application. The Router's job is to format the original location passed in and render the global context NavigationContext, LocationContext * /
export function Router({
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
  static: staticProp = false
}: RouterProps) :React.ReactElement | null {
  // asserts that the Router cannot be inside other routers or an error will be throwninvariant( ! useInRouterContext(),`You cannot render a <Router> inside another <Router>.` +
      ` You should never have more than one in your app.`
  );
  // Format the basename to remove unnecessary/from the URL, such as /a//b to /a/b
  let basename = normalizePathname(basenameProp);
  // Global navigation context information, including route prefixes, navigation objects, etc
  let navigationContext = React.useMemo(
    () = > ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );

  // Convert location. Passing in string will convert it to an object
  if (typeof locationProp === "string") {
    // parsePath is used to convert locationProp to Path objects, both introduced by the history library
    /* interface Path { pathname: string; search: string; hash: string; } * /
    locationProp = parsePath(locationProp);
  }

  let {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default"
  } = locationProp;

  // The real location after base extraction, or null if base extraction fails
  let location = React.useMemo(() = > {
    // stripBasename is used to remove the basename part before pathName
    let trailingPathname = stripBasename(pathname, basename);

    if (trailingPathname == null) {
      return null;
    }

    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key
    };
  }, [basename, pathname, search, hash, state, key]);

  if (location == null) {
    return null;
  }

  return (
    // The only place where location is passed in
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location.navigationType}} / >
    </NavigationContext.Provider>
  );
}
Copy the code

As you can see, the Router component doesn’t have any complicated logic inside it. It just provides the Context and formats the location object that is passed in from the outside.

In addition to using the parsePath method provided by History, normalizePathname and stripBasename are also used.

Here is their source code:

/** * format pathName *@param pathname
 * @returns* /
const normalizePathname = (pathname: string) :string= >
  pathname.replace(/ / / + $/."").replace(/ ^ \ / * /."/");

/** ** Extract basename to get a pure path, or return null * if no match is found@param pathname
 * @param basename
 * @returns* /
function stripBasename(pathname: string, basename: string) :string | null {
  if (basename === "/") return pathname;

  If basename does not match pathName, null is returned
  if(! pathname.toLowerCase().startsWith(basename.toLowerCase())) {return null;
  }

  If basename is included in pathName, the first letter of the pathname is /. If basename is not included in pathname, the first letter of the pathname is /. If basename is not included in pathname, the first letter is /
  let nextChar = pathname.charAt(basename.length);
  if(nextChar && nextChar ! = ="/") {
    return null;
  }

  // return to remove the path of basename
  return pathname.slice(basename.length) || "/";
}
Copy the code

MemoryRouter source code parsing

MemoryRouter is defined in the React-Router package. MemoryRouter is defined in the React-Router package. MemoryRouter is defined in the React-Router package. Memoryrouters, browserRouters and HashRouters work similarly.

import type { InitialEntry, MemoryHistory } from 'history';
import { createMemoryHistory } from 'history';

export interface MemoryRouterProps {
  // Route prefixbasename? :string; children? : React.ReactNode;// Corresponds to the history object parameter returned by createMemoryHistory, which represents the custom page stack and indexinitialEntries? : InitialEntry[]; initialIndex? :number;
}

/** * React-router contains only memoryrouters, and all other routers are in react-router-dom */
export function MemoryRouter({ basename, children, initialEntries, initialIndex }: MemoryRouterProps) :React.ReactElement {
  // a reference to the history object
  let historyRef = React.useRef<MemoryHistory>();
  if (historyRef.current == null) {
    / / create memoryHistory
    historyRef.current = createMemoryHistory({ initialEntries, initialIndex });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });

  // Listen for history changes and reset state after changes
  React.useLayoutEffect(() = > history.listen(setState), [history]);

  // Simply initialize and bind the corresponding state to React
  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}
Copy the code

History. listen resets the current location and action when history.listen detects route changes.

conclusion

  • RouterComponent isreact-routerEssential to an application, usually written directly in the outermost layer of the application, it provides a set of contextual properties and methods about the route jump and state.
  • Usually not directly usedRouterComponent, but usereact-routerInternally suppliedHigh order the RouterComponents, and these higher-order components are, in effect, thehistoryThe navigation objects provided in the library andRouterComponents are connected to control the navigational state of the application.

Router is ready. Configure Route

Let’s look at the official example:

import { render } from "react-dom";
import {
  BrowserRouter,
  Routes,
  Route
} from "react-router-dom";
// These pages don't matter
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";

const rootElement = document.getElementById("root");
render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />} / ><Route path="/expenses" element={<Expenses />} / ><Route path="/invoices" element={<Invoices />} / ></Routes>
  </BrowserRouter>,
  rootElement
);
Copy the code

In our example, we introduced two new components: Routes and Route. We wrote the Route props for each Route url relative to the page, and then passed the Routes as children wrapped in Routes using Routes. Thus, we have an application with a defined route.

Let’s look at an example of nested routines:

<Routes>
  <Route path="/" element={<App />} > {/* The child route is the parent route chilren, and the child route must start with a path that matches the parent route */}<Route path="/expenses" element={<Expenses />} / ><Route path="/invoices" element={<Invoices />} / ></Route>
</Routes>
Copy the code

Inside the App component:

import { Outlet } from 'react-router'
export function App() {
    return (
        <>
          App
          <Outlet/>
        </>)}Copy the code

In the example above we introduced a new component Outlet that renders their child routing elements in the parent routing element.

That is, anything that is matched by the subsequent child route is put into the Outlet component, and when the parent route element renders it internally, it shows the matched child route element. You might wonder how it actually works inside. Don’t worry, for now we’ll just talk about how to render a child path in a page, but we’ll talk more about how it works later.

Route source code

Let’s take a look at the source of the Route component:

// There are three types of props for Route
export interfacePathRouteProps { caseSensitive? :boolean;
  // children represents the child routechildren? : React.ReactNode; element? : React.ReactNode |null; index? :false;
  path: string;
}

export interfaceLayoutRouteProps { children? : React.ReactNode; element? : React.ReactNode |null;
}

export interfaceIndexRouteProps { element? : React.ReactNode |null;
  index: true;
}

/** * The Route component doesn't do anything internally, it just defines props */ for the purpose of using it
export function Route(
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps
) :React.ReactElement | null {
  // The Route cannot be rendered, and the render will throw an error indicating that the Router does not operate on the Route
  invariant(
    false.`A <Route> is only ever to be used as the child of <Routes> element, ` +
      `never rendered directly. Please wrap your <Route> in a <Routes>.`
  );
}
Copy the code

The definition of this component may change most people’s perception of the component. This component is not for rendering interface, it is only a tool to pass parameters in the React-Router (more on Routes later), the only function for the user is to provide an imperative route configuration mode.

The Route component provides three props types, which are the official Route types of the React router: path Route, layout Route, and index Route.

  • Path routing: The most common route definition method. You can define the path to match and whether to allow different case and so on.

    <Routes>
      <Route path="/" element={<App />} / ><Route path="/teams" element={<Teams />} caseSensitive>
        <Route path="/teams/:teamId" element={<Team />} / ><Route path="/teams/new" element={<NewTeamForm />} / ></Route>
    </Routes>
    Copy the code
  • Layout Routing: A way to handle route definitions when there is a common layout that reduces repetitive component rendering, such as the following:

    <Routes>
      <Route path="/" element={<App />} /> {/* Layout route */}<Route element={<PageLayout />} ><Route path="/privacy" element={<Privacy />} / ><Route path="/tos" element={<Tos />} / ></Route>
      <Route path="/contact-us" element={<Contact />} / ></Routes>
    Copy the code

    Otherwise you would probably write something like this:

    <Routes>
      <Route path="/" element={<App />} / >
      {/* Package layout component */}
      <Route
        path="/privacy"
        element={
          <PageLayout>
            <Privacy />
          </PageLayout>} / > {/* Repeat the package layout component */}
      <Route
        path="/tos"
        element={
          <PageLayout>
            <Tos />
          </PageLayout>} / ><Route path="/contact-us" element={<Contact />} / >
    </Routes>
    Copy the code

    As you may have seen, the layout component does not have a path attribute (or you can view path as an empty string). Whether it matches the current pathName depends on its internal child routes. The React-router will skip this route and match its child routes directly. When a child route matches, it searches for the element provided by the parent route from inside to outside from the matched child route before rendering.

    A more detailed understanding can be found in the official documentation.

  • Indexed routing: The most special way to define routes is to enable this route when index is set to true. This route cannot have child routes and the path it can match is always not * from the parent route. For example, /foo/* where indexed routes can match /foo or /foo/). To put it another way, it is equivalent to the index.js file in the directory, which is referenced by default when we import the directory.

    <Routes>
        <Route path="/teams" element={<Teams />} ><Route path="/teams/:teamId" element={<Team />} / ><Route path="/teams/new" element={<NewTeamForm />} / ><Route index element={<LeagueStandings />} / ></Route>
    </Routes>
    Copy the code

    components are rendered when pathname is /teams.

Here is a complete route configuration:

<Routes>
  <Route path="/" element={<App />}> {/* pathName is/by default<Home />* /}<Route index element={<Home />} / ><Route path="/teams" element={<Teams />} ><Route path="/teams/:teamId" element={<Team />} / ><Route path="/teams/:teamId/edit" element={<EditTeam />} / ><Route path="/teams/new" element={<NewTeamForm />} /> {/* pathname is /teams by default<LeagueStandings />* /}<Route index element={<LeagueStandings />} / ></Route>
  </Route>{/* The pathname can be matched only if it is /privacy or /tos<PageLayout />* /}<Route element={<PageLayout />} ><Route path="/privacy" element={<Privacy />} / ><Route path="/tos" element={<Tos />} / ></Route>
  <Route path="/contact-us" element={<Contact />} / ></Routes>
Copy the code

Routes source code

Look at the Route component source code, you will think Routes is not so simple. Internally parsing the props of the Route passed to it.

export interfaceRoutesProps { children? : React.ReactNode;// The user passes in the location object, usually not passed, default is the current browser locationlocation? : Partial<Location> |string;
}

/** * All Routes need Routes packages to render Routes (get the props of the Route, don't render real DOM nodes) */
export function Routes({ children, location }: RoutesProps) :React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
Copy the code

It calls the useRoutes hook and uses the createRoutesFromChildren method to convert children to the useRoutes configuration parameter to get the final routing element.

As for what useRoutes are, I won’t go into details here, but I’ll get to them in a minute. It is a declarative Route generation method (Routes and Routes are imperative) that automatically generates the corresponding render Route elements by passing in an array of configuration objects, and the API is also open to users.

CreateRoutesFromChildren:

// Route configuration object
export interface RouteObject {
  // Whether the route path matches casecaseSensitive? :boolean;
  / / zi lu bychildren? : RouteObject[];// The component to renderelement? : React.ReactNode;// Whether it is an indexed routeindex? :boolean; path? :string;
}

/** * transforms the Route component into a Route object, which is provided to useRoutes using */
export function createRoutesFromChildren(
  children: React.ReactNode
) :RouteObject[] {
  let routes: RouteObject[] = [];

  
       props = 
       props = 
       props
  React.Children.forEach(children, element= > {
    if(! React.isValidElement(element)) {// Ignore non-elements. This allows people to more easily inline
      // conditionals in their route config.
      return;
    }

    // Empty node, ignore it and continue traversing
    if (element.type === React.Fragment) {
      // Transparently support React.Fragment and its children.
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

    // Do not pass other components, only Route
    invariant(
      element.type === Route,
      ` [The ${typeof element.type= = ="string" ? element.type : element.type.name
      }] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`
    );

    let route: RouteObject = {
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      index: element.props.index,
      path: element.props.path
    };

    / / recursion
    if (element.props.children) {
      route.children = createRoutesFromChildren(element.props.children);
    }

    routes.push(route);
  });

  return routes;
}
Copy the code

conclusion

  • react-routerRoutes are defined in two ways:Imperative and declarative, and both are essentially calls to the same route-generating method.
  • RouteCan be thought of as an object that mounts user-passed parameters and is not rendered in the page, but instead isRoutesAccept and parse, we can’t use it alone either.
  • RouteswithRouteStrong binding, yesRoutesMust pass in and only pass inRoute.

Alternative route configuration – useRoutes (Core Concept)

UseRoutes is the core of React – Router V6 and contains a lot of parsing and matching logic.

In the previous section, we used the imperative route configuration mode and found that they were also internally converted by the React router to the declarative route configuration mode, that is, useRoutes was used to create the route. In fact, the React-router exposes the useRoutes method. Users can directly define routes using a declarative notation similar to vue-router.

Example:

import { useRoutes } from "react-router-dom";

// The App returns the rendered route element
function App() {
  let element = useRoutes([
    {
      path: "/".element: <Dashboard />,
      children: [
        {
          path: "/messages".element: <DashboardMessages />
        },
        { path: "/tasks".element: <DashboardTasks />}]}, {path: "/team".element: <AboutPage />}]);return element;
}
Copy the code

As you can see, it’s pretty much the same as it was before.

UseRoutes source code parsing

RouteContext

Here again, we need to introduce a new context-routeconText that stores two properties: outlet and matches.

/** * Dynamic parameter definition */
export type Params<Key extends string = string> = {
  readonly [key in Key]: string | undefined;
};

export interface RouteMatch<ParamKey extends string = string> {
  // params parameters, such as id, etc
  params: Params<ParamKey>;
  // The matched pathname
  pathname: string;
  /** * the child path matches the previous path URL, which can be thought of as the part of the path ending in /* before /* (which is the path of the parent route) */
  pathnameBase: string;
  // Define the route object
  route: RouteObject;
}

interface RouteContextObject {
  // a ReactElement containing the aggregate component of all the child routes inside the Outlet component
  outlet: React.ReactElement | null;
  // An array of successfully matched routes with the index ascending from small to large
  matches: RouteMatch[];
}
/** * Contains all matched routes. */ is not recommended
const RouteContext = React.createContext<RouteContextObject>({
  outlet: null.matches: []});/ * *@internal * /
export {
  RouteContext as UNSAFE_RouteContext
};
Copy the code

RouteContext is officially not recommended for external use either, and you might wonder if the Matches array it provides can be used for something like breadcrumb navigation. Don’t do this, you should use the matchRoutes method provided by the React – Router to manually match, rather than using it. Because the value of the Matches array changes dynamically depending on your routing match hierarchy, you may not get the effect you want.

RouteContext is also one of the keys to route rendering, as you might expect from its outlet property and previous outlet components: its context. Provider is called more than once, but depends on the number of nested layers of child routes.

Split useRoutes

With RouteContext out of the way, let’s get down to business.

The internal logic of useRoutes is quite complex, so let’s take a look at the outermost code and break it down:


/** * 1. This hooks are not called once, but re-call render new Element * 2 every time the route is re-matched. The built-in route context needs to be resolved when useRoutes is called multiple times, inheriting the outer matching result * 3. Internally we calculate the relationship between all routes and the current location, calculate the path weight to get the matches array, and then re-render the array as a nested component */
export function useRoutes(routes: RouteObject[], locationArg? : Partial<Location> |string
) :React.ReactElement | null {
  // useRoutes must have a Router package in the outermost layer, otherwise an error is reported
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useRoutes() may be used only in the context of a <Router> component.`
  );

  Matches is an empty array (default) when this useRoutes is defined for tier 1 routes
  // 2. Matches contains the value when the hooks are rendered in a rendering environment where useRoutes has been called.
  let { matches: parentMatches } = React.useContext(RouteContext);
  // The last route to match (the deepest) will be the parent route, and our subsequent routes will be its children
  let routeMatch = parentMatches[parentMatches.length - 1];
  If useRoutes is called in only one place in the project, it will be the default
  let parentParams = routeMatch ? routeMatch.params : {};
  // The full pathname of the parent route. For example, if the route is set to /foo/* and the current navigation is /foo/1, then parentPathname is /foo/1
  let parentPathname = routeMatch ? routeMatch.pathname : "/";
  // The same parentPathname as above, except for the part before /*, which is /foo
  let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
  let parentRoute = routeMatch && routeMatch.route;
  // Get the location in the context
  let locationFromContext = useLocation();

  // Determine if location is passed in manually, otherwise use the default context location
  let location;
  if (locationArg) {
    // Format it as a Path object
    let parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
    // If location is passed, determine if it matches the parent route (exists as a child route)
    invariant(
      parentPathnameBase === "/"|| parsedLocationArg.pathname? .startsWith(parentPathnameBase),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, ` +
        `the location pathname must begin with the portion of the URL pathname that was ` +
        `matched by all parent routes. The current pathname base is "${parentPathnameBase}"` +
        `but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.`
    );

    location = parsedLocationArg;
  } else {
    location = locationFromContext;
  }

  let pathname = location.pathname || "/";
  ParentMatches = parentMatches = parentMatches = parentMatches = parentMatches
  let remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";
  // Matches the current path, noting that the match is after removing the related path of parentPathname
  
  // Match the rendered route with the current path via the routes configuration item passed in
  let matches = matchRoutes(routes, { pathname: remainingPathname });

  The matches route array and the matches route array of the outer useRoutes are matched
  // React.Element is returned, rendering all matches objects
  return _renderMatches(
    // No matches returns null
    matches &&
      matches.map(match= >
        The inner Route will have all the matching attributes of the outer Route (which is also called the parent Route).
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          The joinPaths function is used to join strings
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase:
            match.pathnameBase === "/"
              ? parentPathnameBase
              : joinPaths([parentPathnameBase, match.pathnameBase])
        })
      ),
    // The parentMatches section is added to the final matches parameter
    parentMatches
  );
}

/** * merge multiple paths into one *@param Paths Path array *@returns* /
const joinPaths = (paths: string[]) :string= >
  paths.join("/").replace(/\/\/+/g."/");
Copy the code

RouteContext = RouteContext = RouteContext = RouteContext = RouteContext = RouteContext;

<Routes>
  <Route path="/" element={<App />}> {/* The parent must be used to match any child routes; otherwise, the react-router route cannot match the nested child routes */}.<Route path="/teams/*" element={<Teams />} / ></Route>
</Routes>
Copy the code

Inside the Teams component:

import { Routes, Route } from 'react-router'
import Team from './Team'
import NewTeamForm from './NewTeamForm'
export function Teams() {
    // Continue to use useRoutes inside the component (as mentioned earlier, using the Routes component calls useRoutes)
    return (
        <Routes>{/* The path prefixes that the parent route already matches are handled internally, so instead of writing /teams/:teamId, write the following */}<Route path="/:teamId" element={<Team />} / ><Route path="/new" element={<NewTeamForm />} / ></Routes>)}Copy the code

This proves that we do not need to define all routes in the outermost layer, we can properly disintegrate the child routes, do some special functions, such as conditional rendering of routes, authentication and so on.

Here’s an overview of what useRoutes does:

  1. Gets the context calluseRoutesIf there is information to prove that the call is used as a child route, the match information of the parent route needs to be merged.
  2. Remove the parent route that has been matchedpathnameAfter prefix, callmatchRoutesWith the current incomingroutesIf the configuration matches, return the matchedmatchesThe array.
  3. call_renderMatchesMethod, render the one obtained in the previous stepmatchesThe array.

The whole process corresponds to three stages: route context resolution stage, route matching stage and route rendering stage.

There is no need to say more about the routing context resolution phase. The following two phases are discussed in detail.

Route matching phase

The route matching phase is actually the process of calling the matchRoutes method. Let’s look at this method:

/** * matches array */ routes and location
export function matchRoutes(
  // Routes object passed in by the user
  routes: RouteObject[],
  // The location currently matched. Note that this is handled first inside useRoutes
  locationArg: Partial<Location> | string.// This parameter is not used inside useRoutes, but the method is exposed, and users can use this parameter to add a uniform path prefix
  basename = "/"
) :RouteMatch[] | null {
  // Format it as a Path object
  let location =
    typeof locationArg === "string" ? parsePath(locationArg) : locationArg;

  As mentioned earlier, extract the basename to get the pure pathname
  let pathname = stripBasename(location.pathname || "/", basename);
  
  // basename fails to match, null is returned
  if (pathname == null) {
    return null;
  }

  // 1. Flattened routes. The routes object is flattened into a one-dimensional array based on the path, containing the weight value of the current route
  let branches = flattenRoutes(routes);
  // 2. Pass in the flattened array and sort by the weight matched internally
  rankRouteBranches(branches);

  let matches = null;
  // 3. Here is the weight comparison after the parsing order, the weight of the first, the first match, then the weight of the lower match
  Terminate the loop when one of the // branches is matched, or none at all
  for (let i = 0; matches == null && i < branches.length; ++i)   {
    // Traverse the flattened routes to see if the path matching rule of each branch matches the PathName
    matches = matchRouteBranch(branches[i], pathname);
  }

  return matches;
}
Copy the code

After looking at the above code carefully, we find that matchRoutes internally divides the route matching into three stages: route flattening, route weight calculation and sorting, route matching and merging.

The route is flat

The route is flattened to better sort the weights. Let’s first look at the comparison before and after the process.

Processing before: After the treatment: You can see that the data structure of the nested tree has been flattened to a one-dimensional array,It’s better for comparison sorting.

Flattening a route is handled in the flattening routes method:

// Route information stored in branch is used for route matching
interface RouteMeta {
  /** * Relative path of the route (excluding the parts that duplicate with the parent route) */
  relativePath: string;
  caseSensitive: boolean;
  /** * The index position defined by the user in the Routes array (relative to its sibling route) */
  childrenIndex: number;
  route: RouteObject;
}

// Flattened route object, containing the full path of the current route object, weighted score and routing information for matching
interface RouteBranch {
  /** * The full path (which incorporates the parent route, the concept of relative route will be introduced below) */
  path: string;
  /** * weight, used to sort */
  score: number;
  /** * Path meta, which is the path rule from parent to child, and the last is the route itself */
  routesMeta: RouteMeta[];
}

/** * Flattening routes flattens all routes into an array for comparing weights *@param The first external call to routes simply passes in this value, the Routes array * used for the transformation@param branches
 * @param parentsMeta
 * @param parentPath
 * @returns* /
function flattenRoutes(
  routes: RouteObject[],
  // routes are used for recursion
  branches: RouteBranch[] = [],
  parentsMeta: RouteMeta[] = [],
  parentPath = ""
) :RouteBranch[] {
  routes.forEach((route, index) = > {
    // Route meta currently managed by Branch
    let meta: RouteMeta = {
      // Save only the relative path, the values here will be processed next
      relativePath: route.path || "".caseSensitive: route.caseSensitive === true.// Index is the order of routes given by the user, which will affect branch sorting to some extent.
      childrenIndex: index,
      // The current route object
      route
    };

    // If route begins with /, it should contain the path of the parent route, otherwise an error is reported
    if (meta.relativePath.startsWith("/")) {
      invariant(
        meta.relativePath.startsWith(parentPath),
        `Absolute route path "${meta.relativePath}" nested under path ` +
          `"${parentPath}" is not valid. An absolute child route path ` +
          `must start with the combined path of all its parent routes.`
      );

      // Remove the parent route prefix as long as the relative path
      meta.relativePath = meta.relativePath.slice(parentPath.length);
    }

    // Full path, incorporating the path of the parent route
    let path = joinPaths([parentPath, meta.relativePath]);
    ParentsMeta = parentsMeta = parentsMeta = parentsMeta = parentsMeta = parentsMeta
    let routesMeta = parentsMeta.concat(meta);

    // Start recursion
    if (route.children && route.children.length > 0) {
      // Index route cannot have childreninvariant( route.index ! = =true.`Index routes must not have child routes. Please remove ` +
          `all child routes from route path "${path}". `
      );

      flattenRoutes(route.children, branches, routesMeta, path);
    }

    // A route without a path (the layout route mentioned earlier) does not participate in route matching unless it is an indexed route
    /* Note: the recursion is done first, which means that the child routes of the layout route will be matched and the child routes will have the routing information of the layout route, which is why the layout route will render properly. * /
    if (route.path == null && !route.index) {
      return;
    }

    // routesMeta, which contains all meta information about the parent route to its own
    // computeScore is a method for calculating weights, which we'll talk about later
    branches.push({ path, score: computeScore(path, route.index), routesMeta });
  });

  return branches;
}
Copy the code

In addition to our concern about how to flatten the route, there is an additional point here – relative routing.

In the previous example I used the absolute path to assign to the path property of Route, but as we can see from the above code, the path property can accept relative paths. Anything that does not start with a/is treated as a relative path. The final matching result will be combined with the path of the parent route (detailed matching code will be shown later in the route matching and merging phase).

<Routes>
  <Route path="/teams" element={<Teams />}> {/* without/is a relative path, inherits the path of the parent route, the effect is the same as /teams/:teamId */}<Route path=":teamId" element={<Team />} / ><Route path="new" element={<NewTeamForm />} / ></Route>
</Routes>
Copy the code
Route weight calculation and sorting

The react-Router defines five different weight units:

// Dynamic route weights, such as /foo/:id
const dynamicSegmentValue = 3;
// Index route weights, that is, routes with index as true
const indexRouteValue = 2;
// Empty route weight, matched when a path value is empty, used only when the last path ends in /
const emptySegmentValue = 1;
// Static route weight
const staticSegmentValue = 10;
// The routing wildcard weight, which is negative, means that we actually reduce the weight when we write *
const splatPenalty = -2;
Copy the code

When we perform path matching, we will split each segment of path according to /, then match the above five weight units in turn, and finally combine all the matching results, which is the final total weight of path.

Let’s look at the computeScore function mentioned earlier:

// Check whether there are dynamic parameters, such as id
const paramRe = /^:\w+$/;
// Check whether the value is *
const isSplat = (s: string) = > s === "*";

/** * Calculates route weights and matches route * static value > params dynamic parameter * according to the weight size@param Path Indicates the complete routing path, not the relative path *@param index
 * @returns* /
function computeScore(path: string, index: boolean | undefined) :number {
  let segments = path.split("/");
  // Initialize the weight value, the number of paths is several, the number of paths has a higher initial weight
  let initialScore = segments.length;
  // There is a * weight minus 2
  if (segments.some(isSplat)) {
    initialScore += splatPenalty;
  }

  // The user passed index, index is a Boolean, representing IndexRouter, weight +2
  if (index) {
    initialScore += indexRouteValue;
  }

  // Filter out the non-* parts
  return segments
    .filter(s= >! isSplat(s)) .reduce((score, segment) = >
        score +
        // If there are dynamic parameters
        (paramRe.test(segment)
          ? // Dynamic parameter weight 3
            dynamicSegmentValue
          : segment === ""
          ? // A null weight of 1. There is only one case where path ends with an extra /, such as /foo versus /foo/
            emptySegmentValue
          : // Static values have a maximum weight of 10
            staticSegmentValue),
      initialScore
    );
}
Copy the code

ComputeScore computeScore computeScore computeScore computeScore computeScore computeScore computeScore computeScore computeScore

Have you ever wondered whether an index route with index set to true will be matched against a child route with the same path as the parent route?

<Routes>
  <Route path="/teams" element={<Teams />}> {/* where path='' is the same as below, but can not write path, otherwise is considered to be layout route, will not join the match, "== null => false */}<Route path="/teams" element={<Team />} / ><Route index element={<LeagueStandings />} / ></Route>
</Routes>
Copy the code

From here you can see that indexed routes will be matched because they pass the same path into computeScore, but with an extra indexRouteValue and a weight of +2.

Review the previous code again:

// 1. Flattened routes. The routes object is flattened into a one-dimensional array based on the path, containing the weight value of the current route
let branches = flattenRoutes(routes);
// 2. Pass in the flattened array and sort by the weight matched internally
rankRouteBranches(branches);
Copy the code

Now that we’ve covered flattening routes and internal weight calculation, a flattening array of routes is generated. The next step is to call the rankRouteBranches method to sort routes:

/** * sort, compare the weight value *@param branches* /
function rankRouteBranches(branches: RouteBranch[]) :void {
  branches.sort((a, b) = >a.score ! == b.score// Sort with the highest weights first
      ? b.score - a.score
      : // if a.core === b.core
        compareIndexes(
          // routesMeta is an array of outermost routes to child routes
          // The childrenIndex is passed in the same order as route in routes, the index is larger
          a.routesMeta.map(meta= > meta.childrenIndex),
          b.routesMeta.map(meta= > meta.childrenIndex)
        )
  );
}


/** * compare the index of the child route to determine whether it is a sibling route. If not, return 0@param a
 * @param b
 * @returns* /
function compareIndexes(a: number[], b: number[]) :number {
  // Whether it is a sibling route
  let siblings =
    // Compare the path of the last route
    a.length === b.length && a.slice(0, -1).every((n, i) = > n === b[i]);

  return siblings
    ? 
      // If it is a sibling node, the order in which a. length-1 and B. length-1 are passed in is the same, but the internal values are different
      a[a.length - 1] - b[b.length - 1]
    : 
      // Only sibling nodes are compared. If not, the weight is the same
      0;
}
Copy the code

Finally, what we get is a sorted flat array.

Route matching and merging

After sorting, the next step is to match the routes. Based on the sorted order, we will match the routes from the lowest index to the highest index, as in the previous matchRoutes code:

let matches = null;
// 3. Here is the weight comparison after the parsing order, the weight of the first, the first match, then the weight of the lower match
Terminate the loop when one of the // branches is matched, or none at all
for (let i = 0; matches == null && i < branches.length; ++i)   {
    // Traverse the flattened routes to see if the path matching rule of each branch matches the PathName
    matches = matchRouteBranch(branches[i], pathname);
}
Copy the code

Here, we call the matchRouteBranch method:

/** * Get the true matches array * from branch and the current pathName@param branch
 * @param routesArg
 * @param pathname
 * @returns* /
function matchRouteBranch<ParamKey extends string = string> (
  branch: RouteBranch,
  pathname: string
) :RouteMatch<ParamKey| > []null {
  let { routesMeta } = branch;

  // Initializes the matched value
  let matchedParams = {};
  let matchedPathname = "/";
  // The final matches array
  let matches: RouteMatch[] = [];
  // Iterate through the routesMeta array, the last item being its route, preceded by parentRoute
  for (let i = 0; i < routesMeta.length; ++i) {
    let meta = routesMeta[i];
    // Is the last route
    let end = i === routesMeta.length - 1;
    // PathName Remaining pathname after the parent route is matched
    let remainingPathname =
      matchedPathname === "/"
        ? pathname
        : pathname.slice(matchedPathname.length) || "/";
    // Use the relative path rule to match the remaining values
    // The matchPath method is used to match individual paths, as discussed below
    let match = matchPath(
      // Only the end of the last route will be true, the rest will be false, where end means the last /
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );

    // If no route is matched, null is returned
    if(! match)return null;

    // the merged params are matched. Note that this is the changed matchedParams, so all params of the route are the same
    Object.assign(matchedParams, match.params);

    let route = meta.route;

    // Complete the path if it matches
    matches.push({
      params: matchedParams,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
      route
    });

    // change the matchedPathname, the already matchedPathname prefix, to be used as a loop for subsequent subroutes
    if(match.pathnameBase ! = ="/") { matchedPathname = joinPaths([matchedPathname, match.pathnameBase]); }}return matches;
}
Copy the code

From the incoming branch we get the routesMeta array that contains all the routing levels, start matching each layer, push the full path and parameters of each match into the Matches array, and return.

The following is the matching flow of the matchPath method:

The whole process is obscure, almost full of regular expressions, accompanied by more complex type gymnastics, the author will try to write clear notes for each stage, if you still can’t understand, you can skip, just need to know the function of matchPath method.

/** * If ts resolves the failed state of the argument, see */ as follows
type ParamParseFailed = { failed: true };

/ * * * this is the type of gymnastics, mainly in analytic params specific parameters, such as parsing / : a, of a get out alone after * ParamParseSegment < > ': / : a/b' = > 'a' | 'b' * /
type ParamParseSegment<Segment extends string> =
  // Check whether there is a path like :id
  // Check here if there exists a forward slash in the string.
  Segment extends `${infer LeftSegment}/${infer RightSegment}`
    ? // If there is a /, it is a URL, start parsing
      // Recursively parse the left side
      ParamParseSegment<LeftSegment> extends infer LeftResult
      // Recursively parse the right side
      ? ParamParseSegment<RightSegment> extends infer RightResult
        ? LeftResult extends string
          ? / / the left parsing is successful, to the right, in the intersection, such as "foo" | "bar"
            RightResult extends string
            ? LeftResult | RightResult
            : LeftResult
          : // If the left parses fail, check whether the right parses succeed. If the right parses fail, ParamParseFailed is returned
          RightResult extends string
          ? RightResult
          : ParamParseFailed
        : ParamParseFailed
      : // If the left side cannot be parsed, the right side can be parsed directly
      ParamParseSegment<RightSegment> extends infer RightResult
      ? RightResult extends string
        ? RightResult
        : ParamParseFailed
      : ParamParseFailed
    : // If there is no /, check whether the parameter meets the following format :id. If so, return the dynamic parameter name. Otherwise, return ParamParseFailed
    Segment extends ` :${infer Remaining}`
    ? Remaining
    : ParamParseFailed;

/** * Parses the given string type, returns string on failure, otherwise returns the union type */ that dynamically references part of the string
type ParamParseKey<Segment extends string> =
  ParamParseSegment<Segment> extends string
    ? ParamParseSegment<Segment>
    : string;
    

/** * THE pattern of a match path, which combines the following three attributes to generate a regular expression to match the path */
export interface PathPattern<Path extends string = string> {
  /** * the Path to be constructed in this mode can be a concrete Path instead of a string, because ts can directly resolve the corresponding argument */
  path: Path; caseSensitive? :boolean;
  /** * ignores trailing slashes when true, otherwise at least the full word boundary */ will be matchedend? :boolean;
}

/** * PathPattern matching information */
export interface PathMatch<ParamKey extends string = string> {
  /** * Dynamic parameters in the path */
  params: Params<ParamKey>;
  /** * Matched path part */
  pathname: string;
  /** * The part of the path that matches before the child path. * /
  pathnameBase: string;
  /** * match pattern */
  pattern: PathPattern;
}

/ / remove readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

/** * Checks whether pathName matches the incoming pattern. If it does not, null is returned. If it does, the parsed value */ is returned
export function matchPath<
  ParamKey extends ParamParseKey<Path>,
  Path extends string> (
  pattern: PathPattern<Path> | Path,
  pathname: string
) :PathMatch<ParamKey> | null {
  / / format
  if (typeof pattern === "string") {
    pattern = { path: pattern, caseSensitive: false.end: true };
  }

  // Compile the parameters of Pattern together into a regular expression matcher for path matching, which captures dynamic paths as groups
  // It also returns an array of params parameter names. Dynamic parameter names such as * and id are in paramNames
  let [matcher, paramNames] = compilePath(
    pattern.path,
    pattern.caseSensitive,
    pattern.end
  );

  // Start matching
  let match = pathname.match(matcher);
  if(! match)return null;

  // The matched pathname
  let matchedPathname = match[0];
  // $1 represents the contents of the NTH parenthesis
  let pathnameBase = matchedPathname.replace(/ (.). / / + $/."$1");
  // Capture the dynamic route array, starting from the second element of the array is matched in (), such as /about/*, passing /about/1 will match 1 (* is also considered dynamic route).
  let captureGroups = match.slice(1);
  // Match all dynamic parameters, including * and :id
  let params: Params = paramNames.reduce<Mutable<Params>>(
    (memo, paramName, index) = > {
      // This is a raw string calculation, because it has been decoded in params later, the pathnameBase fetch will be a problem
      if (paramName === "*") {
        // Corresponds to the matched value
        let splatValue = captureGroups[index] || "";
        /** * this matches the path such as /home/* to /home. * For example, if the Route is set to /home/* and the actual path matches /home/2, the matchedPathname is /home/2 and pathnameBase is /home/* /
        pathnameBase = matchedPathname
          .slice(0, matchedPathname.length - splatValue.length)
          // Remove the trailing /
          .replace(/ (.). / / + $/."$1");
      }

      / / decoding
      memo[paramName] = safelyDecodeURIComponent(
        captureGroups[index] || "",
        paramName
      );
      returnmemo; }, {});return {
    params,
    pathname: matchedPathname,
    pathnameBase,
    pattern
  };
}


/** * decodes the url, does the layer encapsulation, fails to return the passed value *@param value
 * @param paramName
 * @returns* /
function safelyDecodeURIComponent(value: string, paramName: string) {
  try {
    return decodeURIComponent(value);
  } catch (error) {
    warning(
      false.`The value for the URL param "${paramName}" will not be decoded because` +
        ` the string "${value}" is a malformed URL segment. This is probably` +
        ` due to a bad percent encoding (${error}). `
    );

    returnvalue; }}/** * Params * / RegExp => params * /* RegExp => params *@param path
 * @param CaseSensitive is not case-compatible *@param Whether end matches the trailing slash, or the word boundary *@returns* /
function compilePath(
  path: string,
  caseSensitive = false,
  end = true
) :RegExp.string[]] {
  // Path cannot be like /home*, otherwise a warning will be printed
  warning(
    path === "*"| |! path.endsWith("*") || path.endsWith("/ *"),
    `Route path "${path}" will be treated as if it were ` +
      `"${path.replace($/ / \ *."/ *")}" because the \`*\` character must ` +
      `always follow a \`/\` in the pattern. To get rid of this warning, ` +
      `please change the route path to "${path.replace($/ / \ *."/ *")}". `
  );

  // Array of dynamic pathnames
  let paramNames: string[] = [];
  let regexpSource =
    "^" +
    path
      // Ignore the trailing/and /*
      .replace(/ \ \ / * *? $/."") 
      // Make sure to start with a /
      .replace(/ ^ \ / * /."/") 
      // Escape characters specifically related to regular expressions
      .replace(/[\\.*+^$?{}|()[\]]/g."\ \ $&") 
      // Escape path blocks that begin with:, i.e. params, such as :id
      .replace(/:(\w+)/g.(_ :string, paramName: string) = > {
        paramNames.push(paramName);
        return "([^ \ \ /] +)";
      });

  // Handle the trailing /* and * here
  if (path.endsWith("*")) {
    // params has * in it
    paramNames.push("*");
    regexpSource +=
      path === "*" || path === "/ *"
        ? // If it is * or /*, any value is processed
          "$" (. *) 
        : / / (here? :x) is a non-capture match. The capture returns the value in () through match and will not be placed in params
          / / below is match/XXX and / * (/ 0 or more times), the following two overlapping, subsequent official should change, feel strange
          "(? : \ \ / (. +) | \ \ / *) $"; 
  } else {
    // If it doesn't end in *, just ignore the trailing /, otherwise we should match at least one single boundary (compatible with end true, and many more, such as /home/ /home@, i.e., a-z, a-z, 0-9).
    regexpSource += end
      ? "/ * $\ \" 
      : 
        // limits the parent routes to match only its own word. If false, /home can match /home/home2, but not /home2, which means it must have only /home or a prefix of /home/
        // Matches the word boundary
        "(? :\\b|\\/|$)";
  }

  // path => pattern
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

  return [matcher, paramNames];
}
Copy the code

At this point, we have completed all operations in the route matching phase.

Route Rendering phase

Rendermatches the route rendering phase is much less coded than the route matching phase, but can be a bit harder to understand. UseRoutes is internally implemented by calling _renderMatches.

/** * render the Routecontext. Provider component (including multiple nested providers) */
function _renderMatches(
  matches: RouteMatch[] | null.// If called inside a route that already has a match, the match of the parent context is merged
  parentMatches: RouteMatch[] = []
) :React.ReactElement | null {
  if (matches == null) return null;

  // Generate an outlet component. Note that this is from back to reduce, so the match at the top of the index is the outermost match, i.e. the match corresponding to the parent route is the outermost match
  /** * You can see that outlets are components that are generated recursively. The outermost outlet has the most recursive layers and contains all the inner components, * so the < outlet /> we use on the outer layer is an aggregate component with all the child components ** /
  return matches.reduceRight((outlet, match, index) = > {
    return (
      <RouteContext.Provider/ / if you haveelementJust apply colours to a drawingelementIf not filled inelement, the default is <Outlet />Continue rendering inline<Route />children={ match.route.element ! == undefined ? match.route.element :<Outlet />} matches is not globally consistent and displays different values for different levels of RouteContext. The last level is full matches, Value ={{outlet, matches: parentMatches. Concat (matches. Slice (0, index + 1))}} /> RouteContext ={{outlet, matches: parentMatches. Concat (matches.
    );
    // The innermost outlet is null, which is the last child route
  }, null as React.ReactElement | null);
}
Copy the code

In fact, up here_renderMatchesMethod generatedElementOne more than oneRouteContext.ProviderA rough diagram of a polymer is this:The route rendering phase does little more than take what was previously obtainedmatchesThe array is rendered as the React element.

conclusion

  • useRoutesisreact-routerMedium core, whether directly used by usersuseRoutesOr useRouteswithRouteComponent combinations eventually convert to it. thehookHave three stages:Routing context parsing stage, routing matching stage, and routing rendering stage
  • useRoutesIn the context resolution phase it is resolved whether the call has already been made in the outer layeruseRoutesIf called, it will first get the context data of the outer layer, and finally pass in the outer data with the userroutesArrays are combined to produce the final result.
  • useRoutesWill be passed in during the match phaseroutesWith the currentlocation(can be passed manually, but will do internal verification) to do a layer of matching, through the pairrouteIn a statementpathWeight calculation, get the currentpathnameThe best that can be matchedmatchesArray, index from small to large level relationship from outside to inside.
  • useRoutesIn the render phase, it willmatchesThe array is rendered as an aggregateReact ElementThe element as a whole is manyRouteContext.ProviderFrom the outside to the insideFather => son => grandsonSuch a relationship, eachProviderContains two values corresponding to the levelmatchesArray (the last element is of this levelrouteItself) andoutletElements,outletElements are nestedRouteContext.ProviderStorage place, eachRouteContext.ProviderthechildrenisroutetheelementProperties.
  • Every time I useoutletAre actually rendered with built-in routing relationships (if currentrouteThere is noelementProperty is rendered by defaultoutletThat’s why you can write it without it. Rightelementthe<Route/>Component nesting causes) that we can do at the current levelroutetheelementIs used anywhere inoutletTo render child routes.

How are the child paths rendered – Outlet & useOutlet

It should be easy to guess, if you’ve read the previous section, that the child route is rendered using useContext to get the outlet property in RouteconText.provider. Similarly, the react-router provides two calls:
and useOutlet:

import { Outlet, useOutlet } from 'react-router';

function Dashboard() {
  const outlet = useOutlet()
  return (
    <div>
      <h1>Dashboard</h1>{outlet} {/* or */} {/*<Outlet />* /}</div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />} ><Route
          path="messages"
          element={<DashboardMessages />} / ><Route path="tasks" element={<DashboardTasks />} / ></Route>
    </Routes>
  );
}
Copy the code

Also, the React-router allows us to pass context information in outlets:

import { Outlet, useOutlet, useOutletContext } from 'react-router';

function Parent() {
  const [count, setCount] = React.useState(0);
  // The following two methods are equivalent
  // const outlet = useOutlet([count, setCount])
  return <Outlet context={[count, setCount]} / >;
}

// Get the incoming context information in the child route
function Child() {
  const [count, setCount] = useOutletContext();
  const increment = () = > setCount(c= > c + 1);
  return <button onClick={increment}>{count}</button>;
}
Copy the code

Outlet and useOutlet source code analysis

// The context information passed in the outlet
const OutletContext = React.createContext<unknown>(null);

/** * can be used in nested routes, where the context information is the */ passed in by the user when using 
       or useOutlet
export function useOutletContext<Context = unknown> () :Context {
  return React.useContext(OutletContext) as Context;
}


/** * get the current outlet, where you can pass in the context of the outlet */
export function useOutlet(context? : unknown) :React.ReactElement | null {
  let outlet = React.useContext(RouteContext).outlet;
  Outletcontext. Provider is used only when the context has a value. If there is no value, the parent route's outletContext. Provider value is used
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}


export interface OutletProps {
  // You can pass in context information to provide to the elements inside an outletcontext? : unknown; }/** * is to get the current outlet */ on the context
export function Outlet(props: OutletProps) :React.ReactElement | null {
  return useOutlet(props.context);
}
Copy the code

These methods themselves are very simple, so I won’t go into details here.

conclusion

  • react-routerThe use of<Outlet />oruseOutletRender subpaths, which are actually rendered insideRouteContextIn theoutletProperties.
  • <Outlet />anduseOutletContext information can be passed in to be used in child routinguseOutletContextTo obtain. If this parameter is passed, the context information of the parent route will be overwritten. If this parameter is not passed, the context information will be obtained from the inside out.

How to give way to the jump – Navigate & useNavigate

As with child routes, the react-router also provides two modes for route transitions:
and useNavigate.

import { useEffect } from 'react';
import { useNavigate, Navigate } from "react-router";

function App() {
  let navigate = useNavigate();
  // The following two expressions are equivalent
  useEffect(() = > {
      navigate('/foo', { replace: true}}), [])return <Navigate to="/foo" relplace/>
}
Copy the code

We can also pass in relative paths like disk paths, in which the route jumps based on the current location.

import { useNavigate } from "react-router";

function SignupForm() {
  let navigate = useNavigate();

  async function handleSubmit(event) {
    event.preventDefault();
    await submitForm(event.target);
    // Jump to the success path of the parent route of the current route, /auth/signup => /auth/success
    navigate(".. /success", { replace: true });
  }

  return <form onSubmit={handleSubmit}>{/ *... * /}</form>;
}
Copy the code

UseNavigate returns a Navigator function that can be used for programmatic navigation.

Navigate and useNavigate source code parsing

// Define the navigate function returned by useNavigate, which can be passed to or a number to control the display of the browser's page stack
export interfaceNavigateFunction { (to: To, options? : NavigateOptions):void;
  (delta: number) :void;
}

export interface NavigateOptions {
  // Whether to replace the current stackreplace? :boolean;
  // State of the current navigationstate? :any;
}

/** * return the navigate function to pass the same path rules as the navigate folder */
export function useNavigate() :NavigateFunction {
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useNavigate() may be used only in the context of a <Router> component.`
  );
  
  // The Router provides a navigator, which is essentially a history object
  let { basename, navigator } = React.useContext(NavigationContext);
  // Matches object of the current routing hierarchy (as mentioned earlier, this value varies between routecontext.provider hierarchs)
  let { matches } = React.useContext(RouteContext);
  let { pathname: locationPathname } = useLocation();

  // The path before the child path (before /*)
  let routePathnamesJson = JSON.stringify(
    matches.map(match= > match.pathnameBase)
  );

  UseEffect: the page should not jump as soon as it is rendered, it should jump after useEffect
  let activeRef = React.useRef(false);
  React.useEffect(() = > {
    activeRef.current = true;
  });

  // Return the jump function
  let navigate: NavigateFunction = React.useCallback(
    (to: To | number, options: NavigateOptions = {}) = > {
      if(! activeRef.current)return;

      // If it is a number
      if (typeof to === "number") {
        navigator.go(to);
        return;
      }

      // Obtain the actual path. This method is more complicated, we will discuss separately below
      let path = resolveTo(
        to,
        JSON.parse(routePathnamesJson),
        locationPathname
      );

      // add basename to basename
      if(basename ! = ="/") { path.pathname = joinPaths([basename, path.pathname]); } (!!!!! options.replace ? navigator.replace : navigator.push)( path, options.state ); }, [basename, navigator, routePathnamesJson, locationPathname] );return navigate;
}

import type { To } from 'history';

export interface NavigateProps {
  // To is introduced from history
  /* export declare type To = string | PartialPath; * /
  to: To; replace? :boolean; state? :any;
}

/** * Component navigation, with the navigate method called immediately after the page is rendered, is a simple wrap */
export function Navigate({ to, replace, state }: NavigateProps) :null {
  // Must be in the Router context
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of
    // the router loaded. We can help them understand how to avoid that.
    `<Navigate> may be used only in the context of a <Router> component.`
  );

  let navigate = useNavigate();
  React.useEffect(() = > {
    navigate(to, { replace, state });
  });

  return null;
}
Copy the code

Navigate Navigate is also the useNavigate of the call, which processes the path passed in by the user to obtain the final path value and then passes it to NavigationContext to provide the Navigator object.

Path analysis

Here is the detailed code for resolveTo’s internal processing path:

// The Path object is defined in history
import type { Path } from 'history';
/** * resolves to to the actual path to jump to, because to can be a relative path, etc., rather than an absolute path * that begins with/XXX as passed in completely@param ToArg The path to jump to *@param RoutePathnames Path matched by all parent routes *@param LocationPathname The pathname * in the current location@returns* /
function resolveTo(
  toArg: To,
  routePathnames: string[],
  locationPathname: string
  // Return the Path object
) :Path {
  let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
  // If to does not provide a pathname, such as simply changing the search string, return /
  let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname;

  // Which route to navigate from mainly deals with the relative path relationship
  let from: string;
  // To is not provided, from is the current path, does not change the pathname
  if (toPathname == null) {
    from = locationPathname;
  } else {
    // Provide to get rid of... Find from and add to. eliminate
    // Note that routePathnames are externally mapped with matches, and the last segment of the route is the route to useNavigate, not the last segment of the pathName
    /** * if the current pathname is /auth/login, useNavigate is used for the route corresponding to path = /auth, and navigate('.. '), instead of returning to /auth */
    let routePathnameIndex = routePathnames.length - 1;

    if (toPathname.startsWith("..")) {
      let toSegments = toPathname.split("/");

      // The href of the to tag is different from that of the A tag
      while (toSegments[0= = ="..") {
        toSegments.shift();
        // According to toPathname's.. The quantity retracts forward
        routePathnameIndex -= 1;
      }

      to.pathname = toSegments.join("/");
    }

    / / if.. If the number of parent routes exceeds the number of matches, the root path/is returned by default
    from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
  }

  // Note that the to only handles the... We haven't dealt with the situation at the beginning. , including the case of a single
  // Convert to Path object based on to and from
  let path = resolvePath(to, from);

  // If toPathname ends with /, we need to add it here too
  if( toPathname && toPathname ! = ="/" &&
    toPathname.endsWith("/") &&
    !path.pathname.endsWith("/")
  ) {
    path.pathname += "/";
  }

  return path;
}
Copy the code

ResolvePath (resolvePath); resolvePath (resolvePath);

/** * parses the passed Path into a Path object and handles the relationship between relative paths */
export function resolvePath(to: To, fromPathname = "/") :Path {
  let {
    pathname: toPathname,
    search = "",
    hash = ""
  } = typeof to === "string" ? parsePath(to) : to;

  // If to is not a relative path, use it directly. If it is a relative path, handle the relative path
  let pathname = toPathname
    ? toPathname.startsWith("/")? toPathname :// Handle relative paths
        resolvePathname(toPathname, fromPathname)
    : fromPathname;

  return {
    pathname,
    search: normalizeSearch(search),
    hash: normalizeHash(hash)
  };
}

/** ** Process the relative path *@param RelativePath relativePath *@param FromPathname Context path *@returns* /
function resolvePathname(relativePath: string, fromPathname: string) :string {
  // Remove the trailing /, and split with /. Return [""] if fromPathname is passed in as /, note that [""] is the default. Minimum is [""]
  let segments = fromPathname.replace(/ / / + $/."").split("/");
  RelativePath does not start with a slash
  let relativeSegments = relativePath.split("/");

  // This code is the parse path, will.. And. These are compared to parent directories and then resolved to absolute paths
  relativeSegments.forEach(segment= > {
    if (segment === "..") {
      // Keep the root "" segment so the pathname starts at /
      // Note that this is greater than 1. Prove that the relative path is guaranteed to be converted to an absolute path starting with a slash
      if (segments.length > 1) segments.pop();
    } else if(segment ! = =".") {
      // If not, add a new path.. Represents the current path and has no effectsegments.push(segment); }});// Aggregate the array of paths above
  return segments.length > 1 ? segments.join("/") : "/";
}


/** * Format the search string *@param search
 * @returns* /
const normalizeSearch = (search: string) :string= >! search || search ==="?"
    ? ""
    : search.startsWith("?")? search :"?" + search;

/** * Formats the hash string *@param hash
 * @returns* /
const normalizeHash = (hash: string) :string= >! hash || hash ==="#" ? "" : hash.startsWith("#")? hash :"#" + hash;
Copy the code

In general, path resolution is all about converting a relative path into an absolute path that the browser understands.

conclusion

  • react-routerThe use of<Navigate />oruseNavigateJump routes, but the actual internal is usedNavigationContextTo provide thenavigatorObject (i.ehistoryLibrary – provided route jump object).
  • <Navigate />Can pass similar to disk path.with.Is a relative path. Each route segment is split internally in turn to generate an absolute path to jump to.

End of core functionality, additional auxiliary apis

methods

createRoutesFromChildren

Convert the Routes component inside the Routes component to the Routes array in accordance with the useRoutes specification, described in the Routes source code parsing.

generatePath

GeneratePath (Path, params) completes and returns the dynamic parameter of path based on the passed params object.

/ * * * params to be included in the path of the corresponding dynamic parameters, such as / : id / * and {id: 'foo', '*' : 'bar'} = > / foo/bar * /
export function generatePath(path: string, params: Params = {}) :string {
  return path
    .replace(/:(\w+)/g.(_, key) = > {
      // If params does not contain all the dynamic parameters in path, an error is reportedinvariant(params[key] ! =null.`Missing ":${key}" param`);
      returnparams[key]! ; }) .replace($/ / \ \ / * *._= >
      // Check the path is *
      params["*"] = =null ? "" : params["*"].replace(/ ^ \ / * /."/")); }Copy the code

matchRoutes

MatchRoutes (Routes, locationArg, basename) gets a matches array of routes that matches the current location. In useRoutes source parse-route matching phase.

matchPath

MatchPath (Pattern, pathName) determines whether the pathname matches the incoming pattern, returns null if it does not, and matches the parsed match object if it does, as explained in useRoutes source parsing – Route matching phase.

renderMatches

The React Element is used to render the matchRoutes method, which internally calls the _renderMatches method from the previous useRoutes route rendering phase.

Here is the source code:

export function renderMatches(
  matches: RouteMatch[] | null
) :React.ReactElement | null {
  return _renderMatches(matches);
}
Copy the code

resolvePath

ResolvePath (to, from) combines the to and from paths to generate a final path to jump to, as stated in the Navigate and useNavigate source code.

hooks

useInRouterContext

Determine whether the current component is in the Router context, as explained in the Router source code parsing.

useLocation

Get the location of the current browser, as explained in Router source code parsing.

useNavigationType

Get the action type of the current browser jump.

useResolvedPath

UseResolvedPath (to) retrieves the Path passed to based on the current location and returns the Path object.

/ * * * transformation Path of hook, will to format as a Path object * type to = string | PartialPath; * /
export function useResolvedPath(to: To) :Path {
  let { matches } = React.useContext(RouteContext);
  let { pathname: locationPathname } = useLocation();

  // json.stringify is used to avoid generating new address dependencies every time a map is created
  let routePathnamesJson = JSON.stringify(
    matches.map(match= > match.pathnameBase)
  );

  return React.useMemo(
    ResolveTo (resolveTo, resolveTo, resolveTo, resolveTo, resolveTo, resolveTo, resolveTo, resolveTo
    () = > resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
    [to, routePathnamesJson, locationPathname]
  );
}
Copy the code

useHref

UseHref (to) returns a new URL by automatically adding basename to the to path.

/** * Merge context-based basename into full URL to pass current pathName. The official suggestion is to use custom link component so that basename */ can be added automatically
export function useHref(to: To) :string {
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useHref() may be used only in the context of a <Router> component.`
  );

  let { basename, navigator } = React.useContext(NavigationContext);
  let { hash, pathname, search } = useResolvedPath(to);

  let joinedPathname = pathname;
  // get the href with basename
  if(basename ! = ="/") {
    // Format the to argument to get pathName
    let toPathname = getToPathname(to);
    // Does it end with /
    letendsWithSlash = toPathname ! =null && toPathname.endsWith("/");
    joinedPathname =
      pathname === "/"
        ? // If it is /, add basename before it
          basename + (endsWithSlash ? "/" : "")
        : / / merge path
          joinPaths([basename, pathname]);
  }

  // Convert the To object To string
  return navigator.createHref({ pathname: joinedPathname, search, hash });
}

/** * get to pathname *@param to
 * @returns* /
function getToPathname(to: To) :string | undefined {
  // Empty strings should be treated the same as / paths
  return to === "" || (to as Path).pathname === ""
    ? "/"
    : typeof to === "string"
    ? parsePath(to).pathname
    : to.pathname;
}
Copy the code

useMatch

UseMatch (Pattern) is used to determine the status of the component based on the pathname. For example, NavLink displays active status when the incoming pattern matches the current PathName.

/** * Query whether the specified route matches the current pathname */
export function useMatch<
  ParamKey extends ParamParseKey<Path>,
  Path extends string> (pattern: PathPattern<Path> | Path) :PathMatch<ParamKey> | null {
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useMatch() may be used only in the context of a <Router> component.`
  );

  let { pathname } = useLocation();
  return React.useMemo(
    () = > matchPath<ParamKey, Path>(pattern, pathname),
    [pathname, pattern]
  );
}
Copy the code

useParams

UseParams Retrieves all params matched by the current URL.

/** * get all params */ from the current url
export function useParams< // You can manually change the return type by passing generic parametersParamsOrKey extends string | Record<string.string | undefined> = string> () :Readonly< // If the incoming isstringIf the object type is passed in, it is optional. [ParamsOrKey] extends [string]?Params<ParamsOrKey> : Partial<ParamsOrKey>
> {
  let { matches } = React.useContext(RouteContext);
  let routeMatch = matches[matches.length - 1];
  return routeMatch ? (routeMatch.params as any) : {};
}
Copy the code

conclusion

This article is an in-depth analysis of the core principles of React-Router V6. From the beginning of the Router context, spoke two routing configuration mode as well as the realization principle, route how to calculate the weight, how to match and how to apply colours to a drawing (this is the place where the most core), and then to zi lu by rendering principle, then speak to jump two routing method and the analytic results of relative routing.

In addition, we extended the additional exposed auxiliary API of the React-Router to assist users in routing operations.

React-router V6 is a complete upgrade with less code and more functionality, and its core implementation ideas are worth learning. If you want to develop a stand-alone routing library, this might be a good idea.

I was going to cover the react-router-dom and react-router-native together, but since the space is too long and their implementation core relies on the React-router, I’ll leave that for the next article.

reference

  • React-router official document