preface

The React Router is now available in V6. The React Router is now available in V6. React Router history explains how to use the React Router API and how to use the React Router history API. All of the sample code in this article comes from react-router-source-analysis, so you can check it out if anything is unclear. Fasten your seat belt and start the React Router tour

BrowserRouter

We’ll use BrowserRouter as the container for our App

ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>.document.getElementById('root'))Copy the code

Let’s start with the BrowserRouter entry and see what initialization it does:

export function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  const historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    // If it is empty, it is created
    historyRef.current = createBrowserHistory({ window });
  }

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

  React.useLayoutEffect(() = > {
    /** * applyTx(nextAction); /** * applyTx(nextAction); Action) { * action = nextAction; * // Get the current index and location * [index, location] = getIndexAndLocation(); * listeners.call({ action, location }); *} * /
    history.listen(setState)
  }, [history]);
  // The general changes are action and location
  return (
    <Router
      basename={basename}
      children={children}
      action={state.action}
      location={state.location}
      navigator={history}
    />
  );
}
Copy the code

BrowserRouter initialization generates the history instance and sets setState<{action; Note that the process of switching routes is setState by the listeners on the React Router. Note that the process of switching routes is setState by listeners on the React Router.

Router

BrowserRouter finally returns the Router, and the prop it receives changes in is usually the Action and location, and the rest is usually fixed at initialization. Of course, we don’t render the

directly either, but rather the context-specific Router, such as

or

in the browser environment, meaning that this is usually for internal use


export function Router({
  action = Action.Pop,
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  /** is essentially history */
  navigator,
  static: staticProp = false
}: RouterProps) :React.ReactElement | null {

  const basename = normalizePathname(basenameProp);
  const navigationContext = React.useMemo(
    () = > ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );

  if (typeof locationProp === "string") {
    locationProp = parsePath(locationProp);
  }

  const {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default"
  } = locationProp;
  // Replace the pathname passed to location
  const location = React.useMemo(() = > {
    // Get the string after basename in pathName
    const trailingPathname = stripBasename(pathname, basename);

    if (trailingPathname == null) {
      // 1. Pathname does not start with basename
      // 2. Pathname starts with basename, but not with '${basename}/'
      return null;
    }
    // When we get here:
    // 1.basename === "/"
    // 2. Pathname starts with '${basename}/'
    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key
    };
  }, [basename, pathname, search, hash, state, key]);


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

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ action.location}} / >
    </NavigationContext.Provider>
  );
}
Copy the code

The Router finally returns two context. providers, so its children can:

through

const { basename, navigator } = React.useContext(NavigationContext);
Copy the code

Get basename and Navigator from NavigationContext

through

const { location } = React.useContext(LocationContext)
Copy the code

Get the location information.

We’re done with the two contexts, so let’s look at the children, like

The sample

Let’s take the following code example, Take a look at http://localhost:3000/about/child (hereafter abbreviated to/about/child) is how to match to the < the Route path = “/ *” about element = {< about / >} / > element,

}/>

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />} ><Route index element={<Home />} /> {* Note that the suffix must be written as '/*' *}<Route path="about/*" element={<About />} / ><Route path="dashboard" element={<Dashboard />} / ><Route path="*" element={<NoMatch />} / ></Route>
    </Routes>
  );
}

function BasicLayout() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <li>
        <Link to=".">Home</Link>
      </li>
      <li>
        <Link to="about">About</Link>
      </li>.<hr />
      <Outlet />
    </div>
  );
}
function Home() {
  return (
    <h2>Home</h2>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
      <Link to='child'>about-child</Link>
      <Routes>
        <Route path='child' element={<AboutChild/>} / ></Routes>
    </div>
  );
}
function AboutChild() {
  return (
    <h2>AboutChild</h2>); }...Copy the code

Routes and Route

Each Route must be placed in the Routes container. Let’s look at the Routes source

/ * * *@description <Route> elements' container */
export function Routes({ children, location }: RoutesProps) :React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
Copy the code

The children it receives are found below

// Add <> here, otherwise the code is not highlighted
<>
<Route index element={<Home />} / >
<Route path="about" element={<About />} / >
<Route path="dashboard" element={<Dashboard />} / >
<Route path="*" element={<NoMatch />} / >
</>
Copy the code

When we look at Route’s function, it doesn’t render at all

/ * * *@description Not actually rendered, just used to collect props */ for Route
export function Route(
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps
) :React.ReactElement | null {
  // Route has no render, just child of Routes
  // Route must be placed inside Routes, otherwise there will be an error
  // This is the correct way to use it
  // <Route element={<Layout />}>
  // 
      } />
  // 
      } />
  // 
  // </Routes>
  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

Most people think that using a component is a sure way to get into Render, but it’s not. The purpose of Route is to provide props for createRoutesFromChildren to use. This also provides us with a new way of thinking, breaking the previous perception.

createRoutesFromChildren

The createRoutesFromChildren function is used to recursively collect props on the Route, eventually returning a nested array (if the Route has multiple layers).

/ * * *@description Create a route configuration: *@example* RouteObject { * caseSensitive? : boolean; * children? : RouteObject[]; * element? : React.ReactNode; * index? : boolean; * path? : string; *} [] * /
export function createRoutesFromChildren(
  children: React.ReactNode
) :RouteObject[] {
  const routes: RouteObject[] = [];
  React.Children.forEach(children, element= > {
    if(! React.isValidElement(element)) {// Drop non-elements
      // Ignore non-elements. This allows people to more easily inline
      // conditionals in their route config.
      return;
    }

    if (element.type === React.Fragment) {
      / / element for < > < / a >
      // Transparently support React.Fragment and its children.
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

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

    if (element.props.children) {
      /** * if there are children *@example
       * <Route path="/" element={<Layout />}>
       *  <Route path='user/*'/>
       *  <Route path='dashboard/*'/>
       * </Route>
       */
      route.children = createRoutesFromChildren(element.props.children);
    }

    routes.push(route);
  });
  return routes;
}
Copy the code

Let’s log the routes so that it looks clearer

In other words, we do not need to use the form of Routes. We can also directly use useRoutes and pass in an array of nested routines to generate the same Routes element, as shown below

import { useRoutes } from 'react-router-dom'
function App() {
  const routelements = useRoutes([
    {
      element: <BasicLayout />,
      children: [
        {
          index: true.element: <Home />
        },
        {
          path: 'about/*'.element: <Home />
        },
        {
          path: 'dashboard'.element: <Dashboard />}, {path: The '*'.element: <NoMatch />}}]])return (
    routelements
  );
}
Copy the code

useRoutes

Given routes: RouteObject[], useRoutes will useRoutes to match the corresponding route element.

The function can be divided into three sections:

  • Gets the last item of parentMatchesrouteMatch
  • Matches the corresponding through matchRoutesmatches
  • through_renderMatchesRender the one obtained abovematches
export function useRoutes(
  routes: RouteObject[],
  // Routes did not pass locationArg, we ignore thislocationArg? : Partial<Location> | string) :React.ReactElement | null {
  // Get parentMatches, the last routeMatch
  const { matches: parentMatches } = React.useContext(RouteContext);

  const routeMatch = parentMatches[parentMatches.length - 1];
  const parentParams = routeMatch ? routeMatch.params : {};
  const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";



  // Get location from LocationContext
  const locationFromContext = useLocation();

  let location;
  if (locationArg) {
    const parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) : locationArg;

    location = parsedLocationArg;
  } else {
    location = locationFromContext;
  }
  Matches: RouteMatch
      
       []; // remainingPathname = remainingPathname
      
  / / in general, for http://localhost:3000/about/child, the location, the pathname to/auth/child
  const pathname = location.pathname || "/";
  // If parentPathnameBase is not '/', it is truncated from the parentPathnameBase in pathName.
  // Because useRoutes is called in 
      , remainingPathname represents the relative path of the current Routes
  // eg: PathName = '${parentPathnameBase} XXX', remainingPathname = 'XXX'
  // eg: pathName = '/about /child', parentPathnameBase = '/about', remainingPathname = '/child'
  const remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";

  const matches = matchRoutes(routes, { pathname: remainingPathname });
  // Render matches with '_renderMatches'
  return _renderMatches(
    matches &&
      matches.map(match= >
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
        })
      ),
    parentMatches
  );
}
Copy the code

So let’s analyze these three paragraphs separately.

Get the last item of parentMatches routeMatch

We see that we use a Context at the beginning: RouteContext gets the matches type from it and if you look at the code below, we can see from the comments below that this context is actually used in the _renderMatches function at the end of useRoutes, which we’ll talk about in a minute, but just to clarify it, Otherwise you might look a little confused.


typeof RouteContext = {
  outlet: React.ReactElement | null;
  matches: RouteMatch[];
}

/** This is used in _renderMatches. UseOutlet in 'react-router-dom' gets the outlet of the most recent Context */
const RouteContext = React.createContext<RouteContextObject>({
  outlet: null.matches: []});const { matches: parentMatches } = React.useContext(RouteContext);

const routeMatch = parentMatches[parentMatches.length - 1];
// If match, get params, pathName, pathnameBase
const parentParams = routeMatch ? routeMatch.params : {};
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
Copy the code

The first time you enter useRoutes you process the Route under the first Routes

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />} ><Route index element={<Home />} /> {* Note that the suffix must be written as '/*' *}<Route path="about/*" element={<About />} / ><Route path="dashboard" element={<Dashboard />} / ><Route path="*" element={<NoMatch />} / ></Route>
    </Routes>
  );
}
Copy the code

ParentMatches is an empty array, so you get the default values

const parentParams = {};
const parentPathnameBase = "/";
Copy the code

The second time we enter useRoutes we handle the Route under Routes in
:

function About() {
  return (
    <div>
      <h2>About</h2>
      <Link to='child'>about-child</Link>
      <Routes>
        <Route path='child' element={<AboutChild/>} / ></Routes>
    </div>
  );
}
Copy the code

ParentMatches = array length 2; parentMatches = array length 2


const parentParams = {
  params: {* :'child'},
  pathname: "/about/child".pathnameBase: "/about".route: {caseSensitive: undefined.element: {... },index: undefined.path: 'about/*'}};const parentPathnameBase = "/about";
Copy the code

ParentMatches: parentMatches: parentMatches: parentMatches: parentMatches: parentMatches

ParentMatches (_renderMatches) ¶ parentMatches (_renderMatches) ¶

Matches to matches via matchRoutes

Matches :RouteMatch

[] matches:RouteMatch

[

export interface RouteMatch<ParamKey extends string = string> {
  // The key and value of dynamic parameters in the URL
  params: Params<ParamKey>;
  / / the pathname of the route
  pathname: string;
  // The partial URL pathname matched before the child path
  pathnameBase: string;

  route: RouteObject;
}
Copy the code

The entire matchRoutes function looks like this, and is dissected in sections below:

/ * * *@description Based on 'routes' corresponding to' location ', RouteMatch<string>[] '* * Matches the given routes to a location and returns the match data
export function matchRoutes(
  routes: RouteObject[],
  locationArg: Partial<Location> | string,
  basename = "/"
) :RouteMatch[] | null {
  /** get pathName, hash, search */
  const location =
    typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
  // PathName is a relative path taken from basename
  const pathname = stripBasename(location.pathname || "/", basename);

  if (pathname == null) {
    return null;
  }
  // routes can be set to multiple levels, then flatten
  const branches = flattenRoutes(routes);
  /** * Sort branch by score or childrenIndex[@examplePath: '/', Score: 4, routesMeta: [* {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "",caseSensitive: false,childrenIndex: 0} * ] * }, * { * path: '/login', score: 13, routesMeta: [ * {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "login",caseSensitive: false,childrenIndex: 1} * ] * }, * { * path: '/protected', score: 13, routesMeta: [ * {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "protected",caseSensitive: true,childrenIndex: 2}} *] * * / / sorted * *] [* {path: '/ login' score: 13,...}, * {path: '/ protected, score: 13,...} * {path: '/', score: 4, ... }, * ] */
  rankRouteBranches(branches);

  let matches = null;
  Break out of the loop until 'matches' or crossing over' branches' has a value
  for (let i = 0; matches == null && i < branches.length; ++i) {
    matches = matchRouteBranch(branches[i], pathname);
  }

  return matches;
}
Copy the code

A flattener routes collected routeMeta and sorted by rankRouteBranches

Since routes may be a multidimensional array, the routes flatten passed in will be a one-dimensional array first. During the process of flatten, the props of each route will be collected as a routeMeta. The collection process is a depth-first traversal:

/ * * *@description Depthfirst traversal, if route has' children ', children will be processed first and then push the 'route'. * Pay special attention to three points: * - 'LayoutRoute' i.e. only 'element' and 'children', do not branch '* -' route ' Assigns' meta-. RelativePath 'an empty string. =>' RelativePath: The route. The path | | "" ` * - current ` route ` at which level, then get ` branch. RoutesMeta. Length ` for how much * * *@example* <Route path='/' element={<Layout />}> * <Route path='auth/*' element={<Auth />} /> * <Route path='basic/*' Element = {< Basic / >} / > * < / Route > * for branches get [{path: '/ auth / *'...}, {path: '/ Basic / *'...}, {path: '/',...}] * * / / ` LayoutRoute ` only element ` ` and ` children `, * <Route path='auth/*' Element ={< auth/ >} /> * <Route path='basic/*' Element = {< Basic / >} / > * < / Route > * for branches get [{path: '/ auth / *'...}, {path: '/ Basic / *'...}] * /
function flattenRoutes(
  routes: RouteObject[],
  branches: RouteBranch[] = [],
  parentsMeta: RouteMeta[] = [],
  parentPath = ""
) :RouteBranch[] {
  routes.forEach((route, index) = > {
    const meta: RouteMeta = {
      // If path is "", or (LayoutRoute is not written), then relativePath is unified as" ".
      relativePath: route.path || "".caseSensitive: route.caseSensitive === true.childrenIndex: index,
      route,
    };

    if (meta.relativePath.startsWith("/")) {
      /** * If the relative path starts with "/", it indicates that the relative path is an absolute path. The relative path must start with parentPath; otherwise, an error will be reported. * because there is a nested under parentPath routing * eg from SRC/examples/basic/index. The TSX: * 
      }> * // parentPath = '/about', meta. RelativePath = '/ Child ' 
      } /> * // parentPath ='/ about', meta. RelativePath ='/ about/child', * 
      } *  */
      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.`
      );
      // If the path starts with parentPath, the relative path does not need parentPath
      // If eg,path="about" is changed to "/about" by joinPath,
      // meta.relativePath = '/about/child'.slice('/about'.length // 6) = '/child'
      meta.relativePath = meta.relativePath.slice(parentPath.length);
    }
    ParentPath, meta.relativePath with a slash to form an absolute path
    // eg: parentPath = '/', meta.relativePath = 'auhth/*', path = '/auth/*'
    // eg: <Route path="" element={<PublicPage />} /> joinPaths(['', '']) => '/'
    const path = joinPaths([parentPath, meta.relativePath]);
    ParentsMeta is not affected by concat
    // If routesMeta.length > 1, the parentsMeta of the route must precede the last meta
    const routesMeta = parentsMeta.concat(meta);

    // Add the children before adding this route to the array so we traverse the
    // route tree depth-first and child routes appear before their parents in
    // the "flattened" version.
    if (route.children && route.children.length > 0) {
      // If route has children, then it cannot be index route, i.e. its prop index cannot be trueinvariant( route.index ! = =true.`Index routes must not have child routes. Please remove ` +
          `all child routes from route path "${path}". `
      );
      // Where there are children, dispose of children branches first
      flattenRoutes(route.children, branches, routesMeta, path);
    }

    // Routes without a path shouldn't ever match by themselves unless they are
    // index routes, so don't add them to the list of possible branches.
    if (route.path == null && !route.index) {
      // If route does not have path or index route, then branches are not used. Routes where branches are used are LayoutRoute.
      
      }>
      / * * *@example* for examples/auth/index. The TSX, http://localhost:3000/auth * / / the outermost Route (LayoutRoute), there is no path and index, Return * * <Route path="" Element ={<PublicPage />} /> * <Route path="login" element={<LoginPage />} /> * <Route * path="protected" * caseSensitive * element={ * <RequireAuth> * <ProtectedPage /> * </RequireAuth> *} * /> * </Route> * * Branches: * [* {* path: '/', Score: 4, routesMeta: [ * {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "",caseSensitive: false,childrenIndex: 0} * ] * }, * { * path: '/login', score: 13, routesMeta: [ * {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "login",caseSensitive: false,childrenIndex: 1} * ] * }, * { * path: '/protected', score: 13, routesMeta: [ * {relativePath: "",caseSensitive: false,childrenIndex: 0}, * {relativePath: "protected",caseSensitive: true,childrenIndex: 2} * ] * } * ] */
      return;
    }
    // Where all the above conditions have been met, then branches have been added
    // Then routesMeta.length = the number of layers the route itself is in
    branches.push({ path, score: computeScore(path, route.index), routesMeta });
  });

  return branches;
}
Copy the code

When a flattening route obtains branches, it sorts each branch by score or childIndex, and then compares each childIndex of the routesMeta if the score is equal:

/** By 'score' or 'childrenIndex[]' sort 'branch' */
function rankRouteBranches(branches: RouteBranch[]) :void {
  branches.sort((a, b) = >a.score ! == b.score// If not, go first
      ? b.score - a.score // Higher score first
      If score is equal, check if it is siblings. If score is equal, check if it is selfIndex. If score is equal, check if it is siblings
      : compareIndexes(
          a.routesMeta.map(meta= > meta.childrenIndex),
          b.routesMeta.map(meta= > meta.childrenIndex)
        )
  );
}
Copy the code

The branches obtained on the first entry and sorted by rankRouteBranches are as follows

The code is:

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />} ><Route index element={<Home />} /> {* Note that the suffix must be written as '/*' *}<Route path="about/*" element={<About />} / ><Route path="dashboard" element={<Dashboard />} / ><Route path="*" element={<NoMatch />} / ></Route>
    </Routes>
  );
}
Copy the code

The branches obtained the second time are as follows

The code is:

function About() {
  return (
    <div>.<Routes>
        <Route path='child' element={<AboutChild/>} / ></Routes>
    </div>
  );
}
Copy the code

Finally, check whether corresponding matching items can be found according to pathname and each branch. If so, jump out of the cycle

let matches = null;
Break out of the loop until 'matches' or crossing over' branches' has a value
for (let i = 0; matches == null && i < branches.length; ++i) {
  matches = matchRouteBranch(branches[i], pathname);
}

return matches;
Copy the code

matchRouteBranch

MatchRouteBranch looks at each branch’s routesMeta to see if there is a matching pathname, returns null if there is a mismatch, and the last item in the routesMeta is the route’s own routing information. The preceding items are parentMetas. Complete routing information is matched only when the routes are matched from the beginning to the end.

function matchRouteBranch<ParamKey extends string = string> (branch: RouteBranch, pathname: string) :RouteMatch<ParamKey| > []null {
  const { routesMeta } = branch;
  /** Matched dynamic parameters */
  const matchedParams = {};
  /** Indicates the matched path name */
  let matchedPathname = "/";
  const matches: RouteMatch[] = [];
  for (let i = 0; i < routesMeta.length; ++i) {
    const meta = routesMeta[i];
    // The last routesMeta is the current branch's own routeMeta
    const end = i === routesMeta.length - 1;
    // remainingPathname represents the remaining paths that have not yet been matched, because we are using the meta. RelativePath to rematch, so here
    Slice (matchedPathname.length)
    // matchedPathname is not '/', then it is taken from the pathName after matchedPathname
    // eg: PathName = '${matchedPathname} XXX', remainingPathname = 'XXX'
    // eg: matchedPathname = '/', pathName = '/', remainingPathname = '/'
    // eg: matchedPathname = '/', pathName = '/auth', remainingPathname = '/auth'
    const remainingPathname =
      matchedPathname === "/"
        ? pathname
        : pathname.slice(matchedPathname.length) || "/";
    /** * returns {params, pathName, pathnameBase, pattern} or null */
    const match = matchPath(
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );
      // If one of the branches fails to match, the branch will return, meaning that if the last branch fails to match, the branch will still be null
    if(! match)return null;

    Object.assign(matchedParams, match.params);

    // const route = routes[meta.childrenIndex];
    const route = meta.route;

    matches.push({
      params: matchedParams,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
      route
    });

    if(match.pathnameBase ! = ="/") { matchedPathname = joinPaths([matchedPathname, match.pathnameBase]); }}// matches. Length must be equal to routesMeta.length
  return matches;
}
Copy the code

matchPath

Each routeMeta uses the matchPath function to see if it matches, which uses routeMeta’s relativePath(the path we wrote in the Route, such as path = ‘about/*’, Path = ‘child’) , caseSensitive(whether the re generated based on the relativePath is case-insensitive), and end(whether the last routeMeta is the route’s own routing information, which also means that the match is at the end) generate the corresponding re match.

Let’s take a look at matchPath:

/ * * *@description Perform the corresponding re match on pathName to see if match information */ can be returned
export function matchPath<ParamKey extends string = string> (pattern: PathPattern | string, pathname: string) :PathMatch<ParamKey> | null {
  if (typeof pattern === "string") {
    pattern = { path: pattern, caseSensitive: false.end: true };
  }
  // Generate regulars based on pattern. Path and obtain dynamic parameters in path
  // compilePath is covered below, so please read below and come back
  const [matcher, paramNames] = compilePath(
    pattern.path,
    pattern.caseSensitive,
    pattern.end
  );
  // pattern.path Generates whether the regex matches the passed pathname
  const match = pathname.match(matcher);
  if(! match)return null;

  const matchedPathname = match[0];
  // eg: 'about/'.replace(/(.) \/+$/, "$1") => 'about' // , $1 represents the value in the first matching brace;
  // eg: 'about/*'.replace(/(.) \/+$/, "$1") => 'about/*'; // Does not match, returns the original string
  let pathnameBase = matchedPathname.replace(/ (.). / / + $/."$1");
  // eg: pattern = {path: 'about/*', caseSensitive: false, end: true}, pathname = '/about/child';
  // matcher = /^\/about(? :\/(.+)|\/*)$/i, paramNames = ['*'];
  // match = ['/about/child', 'child', index: 0, input: '/about/child', groups: undefined]
  MatchedPathname = '/about/child', captureGroups = ['child'], params = {'*': 'child'}, pathnameBase = '/about'
  // From the second item is matched in (), so it is called slice starting at 1
  const captureGroups = match.slice(1);
  const params: Params = paramNames.reduce<Mutable<Params>>(
    (memo, paramName, index) = > {
      // We need to compute the pathnameBase here using the raw splat value
      // instead of using params["*"] later because it will be decoded then
      if (paramName === "*") {
        const splatValue = captureGroups[index] || "";
        // eg:
        // pattern.path = 'about/*', matchedPathname = '/about/child', captureGroups =['child']
        // matchedPathname.slice(0, matchedPathname.length - splatValue.length) => '/basic/'
        // '/about/'.replace(/(.) \/+$/, "$1") = '/about'
        // that is, pathnameBase = '/about'
        pathnameBase = matchedPathname
          .slice(0, matchedPathname.length - splatValue.length)
          .replace(/ (.). / / + $/."$1");
      }

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

compilePath

MatchPath compilePath uses relativePath, caseSensitive, and End to compile the regex. If path has dynamic parameters, it will collect an array. [‘*’, ‘id’, ‘name’] :

/ * * *@description: Generates the re based on path and gets the dynamic parameter * in path@param {string} Path The path cannot be: XXX *. If the end is *, the end must be "/*" (normal "/", "/auth" is ok)@param {boolean} CaseSensitive defaults to false, depending on whether the path-generated re ignores case *@param {boolean} End defaults to true, whether to the last routesMeta *@return {[RegExp, string[]]} Regress and get the dynamic parameter * * in path@example* * compilePath('/') => matcher = /^\/\/*$/i * compilePath('/', true, false) => matcher = /^\/(? :\b|$)/i * compilePath('/about') => matcher = /^\/about\/*$/i * compilePath('/about/child', true) => matcher = /^\/about\/child\/*$/ * compilePath('about/*', true) => matcher = /^\/about(? : \ / (. +) | \ $/ * / / *)
function compilePath(
  path: string,
  caseSensitive = false,
  end = true
) :RegExp.string[]] {
  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 parameter names
  // eg: '/auth/:id/www/:name/ee' => paramNames = ['id', 'name']
  const paramNames: string[] = [];
  let regexpSource =
    "^" +
    path
      .replace(/ \ \ / * *? $/."") // drop '/', '//'... , or '/*', '//*', '///*'... The '*'
      .replace(/ ^ \ / * /."/") // start with no '/' then add; If there are more than one '/' at the beginning, keep one; eg: (//about | about) => /about
      .replace(/[\\.*+^$?{}|()[\]]/g."\ \ $&") / / to \. * + ^ $? \(\)\[]' => '\(\)\[]'; `. * ^ + $? {} ` = > '\. \ * \ + \ ^ $\ \? The \ {\} '
      .replace(/:(\w+)/g.(_: string, paramName: string) = > {  // \w === [a-zA-z0-9_], / (\w+)/g indicates processing dynamic parameters
        paramNames.push(paramName);
        /** [^\ /]+ indicates that /* cannot occur@example
         * '/auth/:id/www/:name/ee' => '/auth/([^\/]+)/www/([^\/]+)/ee'
         * const reg = new RegExp('/auth/([^\/]+)/www/([^\/]+)/ee', 'i')
         * reg.test('/auth/33/www/a1_A/ee') // params = ['33', 'a1_A'], true
         * reg.test('/auth/33/www/a1_A//ee')) // params = ['33', 'a1_A/'], false
         */
        return "([^ \ \ /] +)";
      });

  if (path.endsWith("*")) {
    // If path ends with "*", then paramNames also push
    paramNames.push("*");
    regexpSource +=
      // If path is equal to * or /*, then regexpSource will end up regexpSource = '^/(.*)$',(.*)$represents the rest of the match
      path === "*" || path === "/ *"
        ? "$" (. *) // Already matched the initial /, just match the rest
        / * * *? :x), matches 'x' but does not remember the match. These parentheses, called non-capture parentheses, allow you to define subexpressions to be used with regular expression operators. *@example* eg1: * /(? : {1, 2} / foo). If the expression is /foo{1,2}/, {1,2} will only be applied to the last character 'o' of 'foo'. * Const reg = new RegExp('w(?)) * const reg = new RegExp('w(?)); :\\d+)e') * reg.exec('w12345e') * ['w12345e', index: 0, input: 'w12345e', groups: Const reg = new RegExp('w(\\d+)e') * reg.exec('w12345e') * ['w12345e', '12345', index: 0, input: 'w12345e', groups: undefined] // remember the matching item * * local eg: * path = 'XXX' /*' * const reg = new RegExp(" XXX (? : \ \ / (. +) | \ \ / *) $", ABC is under the 'I') * (. +) in the * reg exec (' XXX/ABC) / / [' XXX/ABC ', 'ABC' index: 0, input: 'XXX/ABC' groups: Undefined] * below two meet ` | ` behind \ \ / * : '/' appear in zero or more times * reg exec (' XXX ') / / [' XXX ', undefined, the index: 0, input: 'XXX' groups: undefined] * reg.exec('xxx/') // ['xxx/', undefined, index: 0, input: 'xxx/', groups: Undefined] * when > = 2 '/', and then become meet \ \ / (. +), so the individual feels here should instead \ \ \ \ / * / {0, 1}?????? * reg.exec('xxx//') // ['xxx//','/', index: 0, input: 'xxx//', groups: undefined] */
        : "(? : \ \ / (. +) | \ \ / *) $"; // Don't include the / in params["*"]
  } else {
    // Path does not end with "*"
    regexpSource += end
      ? "/ * $\ \" // When matching to the end, ignore trailing slashes
      : // Otherwise, at least match a word boundary. This restricts parent
        // routes to matching only their own words and nothing more, e.g. parent
        // route "/home" should not match "/home2".
        /** * Otherwise, at least one word boundary is matched, which limits parent routes to match only its own word. For example, /home cannot match /home2. * * \b: Matches A position in which the preceding and following characters are not all (one is, one is not or does not exist) \w (\w === [A-za-z0-9_]) * in common sense, \b is "implicit position" * the 'I' and 't' in "It" are the display position, with the "implicit position" in the middle. More visible: https://www.cnblogs.com/litmmp/p/4925374.html * the use of "moon", for example: * / \ bm/match the "m" in "moon"; * /oo\b/ does not match the 'oo' in "moon" because 'oo' is followed by a 'word' character 'n' * /oon\b/ matches the 'oon' in "moon" because 'oon' is the end of the string. So he is not followed by a "word" character * * this example: * compilePath('/', true, false) => matcher = /^\/(? :\b|$)/i * '/auth'.match(/^\/(? :\b|$)/i) // ['/', index: 0, input: '/auth', groups: undefined] * 'auth'.match(/^\/(? :\b|$)/i) // null * reg.exec('/xxx2') or reg.exec('/xxxx') // null * */
        "(? :\\b|$)";
  }

  const matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

  return [matcher, paramNames];
}
Copy the code

There are many functions called in matchRoutes, so here’s a summary:

  1. First, through theflattenRoutescollectroutesMetaTo eachbranchesMedium, then passrankRouteBranchesThe sorting
  2. Then throughmatchRouteBranchFind the corresponding array of matches for each branch and pathnamematchesIf you find it, you jump out of the loop
  3. matchRouteBranchWill traverse each branchroutesMetaThrough thematchPathIn the callcompilePathGenerate the re and get the dynamic parameters in pathparamNamesOnly allroutesMetaThe branch matches only when both are matched

Ps: This code will be a bit complicated, and there may be some confusing places in the process, especially when it comes to re matching. To really understand it, I still need to verify it through the debugger, but I believe that AS long as I read it several times, I can understand it well

Render matches above with _renderMatches

Ok, we finally got through the second step, so the third step is about to see the light of hope ~~

Matches: render (matches); render (matches);

return _renderMatches(
  matches &&
    matches.map(match= >
      Object.assign({}, match, {
        params: Object.assign({}, parentParams, match.params),
        pathname: joinPaths([parentPathnameBase, match.pathname]),
        pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
      })
    ),
  parentMatches
);
Copy the code

_renderMatches renders RouteconText.provider from right to left, according to the match and parentMatches, from child –> parent.

/** Render a nested '< routecontext. Provider>
      ' */ based on matches
function _renderMatches(
  matches: RouteMatch[] | null,
  parentMatches: RouteMatch[] = []
) :React.ReactElement | null {
  if (matches == null) return null;
  return matches.reduceRight((outlet, match, index) = > {
    // If mate.route. element is empty, then 
       is actually the Outlet of the RouteContext, which is the Outlet of the value below
    return (
      <RouteContext.Provider
        children={match.route.element || <Outlet />}
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1))
        }}
      />
    );
  }, null as React.ReactElement | null);
}
Copy the code

Its generation structure is similar:

Matches' index + 1 'generates a Provider as an outlet to the index Provider value:
// matches.length = 2
return (
  <RouteContext.Provider
    value={{
      matches: parentMatches.concat(matches.slice(0.1)),
      outlet:(<RouteContext.Provider
        value={{
          matches: parentMatches.concat(matches.slice(0.2)),
          outlet: null/ / for the first timeoutletfornull,}} >
        {<Layout2 /> || <Outlet />}
      </RouteContext.Provider>),}} > {<Layout1 /> || <Outlet />}
  </RouteContext.Provider>
)
Copy the code





export function Outlet(_props: OutletProps) :React.ReactElement | null {
  return useOutlet();
}

export function useOutlet() :React.ReactElement | null {
  return React.useContext(RouteContext).outlet;
}
Copy the code

returns the Outlet of the nearest layer of RouteContext.

The child routeconText. Provider generated by _renderMatches will act as an outlet to the previous parent, And because the children of the current Routecontext. Provider are elements or
of its match,













function BasicLayout() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <li>
        <Link to=".">Home</Link>
      </li>
      <li>
        <Link to="about">About</Link>
      </li>.<hr />
      <Outlet />
    </div>
  );
}

Copy the code

Finally, the matching process of Route is finished. The following will talk about some hooks and components that are commonly used but not mentioned above.

useNavigate

V6 replaces useHistory with useNavigate, which returns a navigate method that is simple to implement:

  • fromNavigationContextgetnavigator, which is the history instance.
  • Then according to theto,matchesEach of thepathnameBaseAnd the current URL pathName generates the final path path({pathname, search, hash})
  • Depending on whether or notreplaceTo determine whether to call the replace or push method
export function useNavigate() :NavigateFunction {
  const { basename, navigator } = React.useContext(NavigationContext);
  const { matches } = React.useContext(RouteContext);
  const { pathname: locationPathname } = useLocation();
  // Stringgify is for the following memo??
  const routePathnamesJson = JSON.stringify(
    matches.map(match= > match.pathnameBase)
  );
  /** Whether */ has been mounted
  const activeRef = React.useRef(false);
  React.useEffect(() = > {
    activeRef.current = true;
  });

  const navigate: NavigateFunction = React.useCallback(
    (to: To | number, options: { replace? : boolean; state? : State } = {}) = > {
      warning(
        activeRef.current,
        `You should call navigate() in a React.useEffect(), not when ` +
          `your component is first rendered.`
      );

      if(! activeRef.current)return;

      if (typeof to === "number") {
        navigator.go(to);
        return;
      }

      const path = resolveTo(
        to,
        JSON.parse(routePathnamesJson),
        locationPathname
      );

      if(basename ! = ="/") {
        path.pathname = joinPaths([basename, path.pathname]);
      }
      // Replace is called only if replace is true, otherwise it's push(!!!!! options.replace ? navigator.replace : navigator.push)( path, options.state ); }, [basename, navigator, routePathnamesJson, locationPathname] );return navigate;
}
Copy the code

useLocation

UseLocation is getting a location from the LocationContext. The LocationContext is called on the Router, so if you don’t notice it, you can scroll up to it

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

useResolvedPath

UseResolvedPath resolves the pathname of the given to based on the current location and matches of the RouteContext

export function useResolvedPath(to: To) :Path {
  const { matches } = React.useContext(RouteContext);
  const { pathname: locationPathname } = useLocation();
  // Change the memo dependency to a string to avoid invalidation of the cache.
  const routePathnamesJson = JSON.stringify(
    matches.map(match= > match.pathnameBase)
  );

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

useParams

As the name suggests, used to get params

export function useParams<Key extends string = string> () :Readonly<
  Params<Key>
> {
  const { matches } = React.useContext(RouteContext);
  const routeMatch = matches[matches.length - 1];
  return routeMatch ? (routeMatch.params as any) : {};
}
Copy the code

useLinkClickHandler

UseLinkClickHandler is used to handle the click behavior of the routing component, and is suitable for customizable components, because it returns the same click behavior as

export function useLinkClickHandler<
  E extends Element = HTMLAnchorElement.S extends State = State> (to: To, { target, replace: replaceProp, state }: { target? : React.HTMLAttributeAnchorTarget; replace? : boolean; state? : S; } = {}) : (event: React.MouseEvent<E, MouseEvent>) = >void {
  const navigate = useNavigate();
  const location = useLocation();
  const path = useResolvedPath(to);

  return React.useCallback(
    (event: React.MouseEvent<E, MouseEvent>) = > {
      if (
        event.button === 0 && // Ignore everything but left clicks(! target || target ==="_self") && // Let browser handle "target=_blank" etc. Let the browser handle "target=_blank" and so on! isModifiedEvent(event)// Ignore modifier keys Meta, Alt, CTRL, shift, and click click
      ) {
        event.preventDefault();

        // If the URL hasn't changed, a regular <a> will do a replace instead of
        // a push, so do the same here.
        // If replace is passed true or the current location is equal to the 'pathname + search + hash' of the passed path, replace is true,
        //  will use replace instead of push if the URL is not changed
        UseResolvedPath (to) {pathName: '/basic', search: '', hash: ''}
        // Replace should be createPath(location) === createPath(path), which is true. Replace should be false, which is push
        constreplace = !! replaceProp || createPath(location) === createPath(path); navigate(to, { replace, state }); } }, [location, navigate, path, replaceProp, state, target, to] ); }Copy the code

After talking about common hooks, let’s talk about some common components.

<Link />

Link essentially wraps the and handles the onClick event. If you define onClick, use that onClick, otherwise use the internal function internalOnClick

export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  function LinkWithRef(
    { onClick, replace = false, state, target, to, ... rest }, ref) {
    const href = useHref(to);
    const internalOnClick = useLinkClickHandler(to, { replace, state, target });
    function handleClick(
      event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
    ) {
      // If onClick is passed, call that onClick
      if (onClick) onClick(event);
      Otherwise, if the default behavior of the event is not blocked, call internalOnClick,
      // Because internalOnClick calls event.preventDefault() so event.defaultprevent = true
      if (!event.defaultPrevented) {
        internalOnClick(event);
      }
    }

    return (
      // eslint-disable-next-line jsx-a11y/anchor-has-content
      <a
        {. rest}
        href={href}
        onClick={handleClick}
        ref={ref}
        target={target}
      />); });Copy the code

<Navigate />

Navigate is used to change the current location, and is more commonly used in class components to navigate to the corresponding TO after useEffect. UseNavigate is recommended for function components

export function Navigate({ to, replace, state }: NavigateProps) :null {
  const navigate = useNavigate();
  React.useEffect(() = > {
    navigate(to, { replace, state });
  });

  return null;
}
Copy the code

conclusion

React Router matches useNavigate, useLocation, and the components and
. Clone the branch feature/examples-source-analysis to follow the debugger. If you don’t understand something, you can clone the branch feature/examples-source-analysis to follow the debugger.

The last

This is the last article to analyze the React Router source code. Thank you for reading this article.

Thank you for leaving your footprints. If you think the article is good 😄😄, please click 😋😋, like + favorites + forward 😄

The articles

Translation translation, what is ReactDOM. CreateRoot

Translate translate, what is JSX

React Router V6 is now available.

React Router source code history