A preface

React-router v6 is now available in version 6. Many students may find that V6 is completely different from v5, because recently they came into contact with the new project of React and used react-Router V6. Is this the same router I know? There are major changes from API to principle, so today we will take a look at the changes in the new routing.

My advice for this change is to try a new version of Rouer for a new project, but not to upgrade v6 for an old project because it will require a lot of functionality changes, and if you use third-party libraries that depend on the Router, they may fail. As a result, some third party libraries that rely on react-Router will also need to be updated to accommodate version v6, such as my react-Keepalive-router, which cache pages, will also get a major update.

Through this chapter, you will learn the following:

  • Differences in routing, usage, and API between the new version and the old version.
  • The new routing components Router, Routes, and Route work.
  • Principles of Outlet components.
  • UseRoutes principle.

Let’s begin today’s Router V6 learning tour. Send rose 🌹, the hand has lingering fragrance, hope to see the feel of the harvest of the students, can give the author praise βž• pay attention to a wave, in order to encourage me to continue to create front-end hard text.

Ii Basic Use

First let’s look at the changes in V6 from the way routing is used, or an example of a scenario. For example, there is the following routing structure:

As shown in the figure above, the page is divided into a simple 2-level routing structure:

  • The first level page hashomePage,listPage, andchildrenPage.
  • The second level page is the child path of the Children page, including:child1 ε’Œ child2.

Let’s take a look at how the old and new versions of routing are used differently.

1 Route of the old version

Configure routes of the earlier version

Entry file -> Level 1 Routing

const index = () = > {
  return <div className="page" >
    <div className="content" >
      <BrowserRouter>
         <Menus />
         <Switch>
            <Route component={Children}/ *childrenComponent * /path="/children"
            ></Route>
            <Route component={Home}/ *homeComponent * /path={'/home'}
            ></Route>
            <Route component={List}/ *listComponent * /path="/list"
            ></Route>
         </Switch>
      </BrowserRouter>
    </div>
  </div>
}
Copy the code

The preceding is the case of the configured level-1 route. Let’s look at the general distribution of functional responsibilities:

  • BrowserRouter: Passes through the history libraryhistoryObject,locationObject.
  • Switch: Matches a unique routeRouteTo display the correct routing components.
  • RouteThe view hosts the container and controls renderingUIComponents.

The above is the configuration of the primary route and corresponding components. Next, let’s look at the configuration of the secondary route. The secondary route is configured in Children:

function Children (){
    return <div>Here is the Children page<Menus />
       <Route component={Child1}
           path="/children/child1"
       />
       <Route component={Child2}
           path="/children/child2"
       />

    </div>
}
Copy the code
  • You can see in theChildrenThere,Child1 ε’Œ Child2Two components.

Take a look at the overall effect:

The overall routing hierarchy structure is shown below (focusing on the differences with v6’s overall design) :

Route status and page redirect

V5 can obtain the route status in the following ways

  • Props + Route: The UI component supported by Route can obtain the routing state through props. If you want to pass the routing state to descendant components, you can pass the routing state layer by layer through props.
  • withRouter: withRouter is a higher-order component HOC, because the default is only HOCRouteThe component of the package can only get the routing state. If the current non-routed component wants to get the state, it can get it through the withRouter packagehistory ,locationAnd other information.
  • useHistory: Function components can passuseHistoryTo obtainhistoryObject.
  • useLocation: Function components can passuseLocationTo obtainlocationObject.

The V5 uses the following methods to redirect routes

Route state acquisition was introduced above, and another scenario is route switching, so V5 mainly changes the route in two ways:

  • throughreact-router-domThe built-inLink.NavLinkComponent to implement route jumps.
  • throughhistoryObject under the route jump method, such as push, to achieve the route jump.

Overall architecture design

Routing state transfer

In React, routing state is transmitted through a Context. As we all know, in React, Context is a great way to transmit state. In Router, Context is also transmitted through a Router. In React-Router V5.1.0 and earlier, information such as the history and Location object is passed through a RouterContext.

In V5.2.0 to new V5 React-Router, history state is stored separately by HistoryContext in addition to RouterContext.

The overall design of routing module

Let’s take a look at the overall design of v5’s React-Router:

The above is the module design of the whole React – Router V5.

2 V6 Router

Next, we tried the React – Router V6 version. Use V6 to achieve the above functions.

Route configuration of the new version

Entry File -> Overall Routing configuration

import { Routes , Route , Outlet  } from 'react-router
import { BrowserRouter } from 'react-router-dom' const index = () => { return 
      
} path="/home" > } path="/list" > } path="/children" > } path="/children/child1" > } path="/children/child2" >
}
Copy the code

As mentioned above, we also implemented nested secondary routing with the V6 router. Through the above code, we can conclude:

    1. In v6BrowserRouter ε’Œ HashRouterIt’s still at the top of the application. Provides core objects such as history.
    1. In the new version of the router, there is no matching unique routeSwitchComponent, insteadRoutesComponent, but we cannot use Routes as a substitute for Switch. In the new architecture, Routes plays an important role in theReact-router Routing principleIn this article, we explained that the Switch can match a unique Route component to render based on the current Route path. The Switch itself can be discarded, but Routes play an important role in the new version. For example, Route can be used directly without Switch in V5, but Route can be used in V6. The outer layer must be added with Routes component, that is, the combination of Routes -> Route.

If there is no Routes around the Route, an error is reported. Such as the following

This is something that you should pay attention to when developing.

    1. For newer versions of routing, the nested routing structure will be clearer. For example, in the old version of routing, configuring the secondary routing needs to be configured in the business component, as in the first example, we need to configure in the business componentChildrenConfigure the secondary route in the component. In V6, however, there has been an improvement in configuring child routes, which can be written directly to the Route component, such as GeneralChild1 ε’Œ Child2I wrote it right there/childrenWhere will the child routes be renderedLayoutThe component. Take a look at how Layout renders the child routing components.

Layout -> Render secondary routing

function Container(){
  return <div> <Outlet/></div>
}
/* Subroute menu */
function Menus1(){
  return <div>
      <Link to={'/children/child1'} > child1 </Link>
      <Link to={'/children/child2'} > child2 </Link>
  </div>
}

function Layout(){
  return <div>Here is the Children page<Menus1 />
     <Container />
  </div>
}
Copy the code
  • As you can see, Layout does not render a subpath directly, but only oneContainerThe Container uses the V6 Router in the ContainerOutlet. The Outlet is where the child routes are actually rendered, which is Child1 and Child2. The Outlet here is more like an identity card, proving that this is where the real routing component is mounted, and it is not affected by the component hierarchy (as you can see directly from above, the Outlet is not inside the Layout, but in the Container). This way, it is clearer and more flexible. The ability to render components to any node in the subcomponent tree.

So to summarize the routing structure diagram, see the following:

From the above comparison, you can see the general differences between V6 and V5. Here is a summary of the functional aspects:

    1. Newer versions of the Router do not existSwitchComponent, replaced by Routes, but in terms of functionRoutesIs central, plays an indispensable role. The route of the old version can be used independently. The route of the new version must be used together with Routes.
    1. The new version of routing introduces the Outlet placeholder function, which facilitates the configuration of the routing structure. Unlike the old version of routing, subroutes are configured in specific service components, which is clearer and more flexible.

Let’s take a look at the other features of V6.

Route status and page redirect

Obtain route status and redirect the page

    1. State to obtain: For route state location, use custom hooksuseLocation. The location which stores the hash | key | pathname | search | state, etc.

    1. Routing hop: New routing is provideduseNavigateTo redirect routes. Refer to the following code for specific usage:
function Home (){
    const navigate = useNavigate()
    return <div>
       <button onClick={()= >Navigate ('/list',{state:'alien'})} > The jump list page</button>
    </div>
}
Copy the code

Navigate: The first parameter is the forward path, and the second parameter is the route status information that is described, which can convey information such as state.

    1. Dynamic routing: The new version implements dynamic routing and is flexible. You can use useParams to obtain dynamic routing information from urls. For example:

Configuration:

<Route element={<List/>} path="/list/:id"></Route>
Copy the code

Switch to the dynamic routing page:

<button onClick={() = >{ navigate('/list/1'})}} > Jump list page </button>Copy the code

UseParams Obtains dynamic route parameters

function List(){
    const params = useParams()
    console.log(params,'params') // {id: '1'} 'params'
    return <div>
        let us learn React !
    </div>
}
Copy the code
    1. Url parameter information acquisition:A new version of the route is provideduseSearchParamscanTo obtain | Set up theThe url parameter. Here’s an example:
function Index(){
    const [ getParams ,setParam] = useSearchParams()   // The first parameter getParams gets the url information such as param, and the second parameter setParam sets the URL information such as param.
    const name = getParams.getAll('name')
    console.log('name',name)
    return <div>
        hello,world
        <button onClick={()= >{setParam({name:'alien', age: 29}) // can set the url param information}} > setParam</button>
    </div>
}
Copy the code

UseSearchParams returns an array.

  • The first entry in array 1,getParamsObtain URL parameter information.
  • The second entry in array 2,setParamExample Set URL parameters.

Take a look at the demo:

    1. Configuration is more flexible.In version v5, you can configure routing components using an additional routing plug-in called Optionsreact-router-configIn therenderRoutesMethods. Custom hooks are provided in version v6useRoutesThe yield configuration is more flexible. Let’s see how it works.
const routeConfig = [
  {
     path:'/home'.element:<Home />
  },
  {
     path:'/list/:id'.element:<List />
  },
  {
     path:'/children'.element:<Layout />,
     children:[
       { path:'/children/child1' , element: <Child1/> },
       { path:'/children/child2' , element: <Child2/>}}]]const Index = () = > {
  const element = useRoutes(routeConfig)
  return <div className="page" >
    <div className="content" >
        <Menus />
        {element}
    </div>
  </div>
}

const App = () = > <BrowserRouter><Index /></BrowserRouter>
Copy the code

The above makes the structure clearer and configuration more flexible.

    1. In addition, V6 provides hooks for several other functions, which will not be covered here. If you are interested, check out the official documentation, Portal.

Overall architecture design

Let’s take a look at the overall v6 design:

  • As you can see in the figure above, the new version v6 has fully embraced hooks.
  • A number of contexts are passed, such as navigate (functionally the old version of history)) NavigationContextObject, the LocationContext object that passes location, etc.

Three-principle analysis

The above describes the differences in usage between V5 and V6 versions of routing. Next, let’s focus on how the new Route works. And how it differs from the old version.

1 New Route design

In the old version of routing, the core component is Route. As described in the previous article on the Principle of routing, Route is consumed by context internally. When the Route changes, the Route consuming context is re-rendered. So the core component to actually match and mount is Route.

In the new version of Route, the logic for routing updates, Route matching, and rendering actual page components is handed over to Routes, and a branch is added. The new version of the routing structure can be interpreted as a hierarchical tree structure, that is, when the route changes, the Routes will be found in the routing structure tree, the need to render branch. The main purpose of the Route component at this point is just to form each node in the routing tree structure, but there is no actual rendering of the page.

The routing of the new version decouples routes from service components. The configuration of routes is handled through the outer routing structure tree instead of inside the service components to be specified. For views it is passed layer by layer through OutletContext, so let’s look at the details.

2 the outer container, update the source BrowserRouter | HashRouter | the Router

In the new version of routing, the outer Router components are different from those in the old version. Using BrowserRouter as an example, take a look at the older version.

Older versions of BrowserRouter

import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
  history = createHistory(this.props) 
  render() {
    return <Router history={this.history} children={this.props.children} />; }}Copy the code
  • Older versions of BrowserRouter do just thatcreateHistorycreatehistoryObject that is then passed to the Router component.

Now, the new version of BrowserRouter, what does it do?

react-router-dom/index.tsx

export function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  /* Save the history object with useRef */
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  /* history change, notification update. * /
  React.useLayoutEffect(() = > history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}
Copy the code

The new version of BrowserRouter does the following:

  • throughcreateBrowserHistorycreatehistoryObject and passuseRefSave the history object.
  • throughuseLayoutEffectTo listen tohistoryChanges when history changes (browser input, get a tag jump, API jump, etc.). Distribute updates and render the entire Router tree.This is different from older versions where the update component listens for route changes in the Router.
  • One other thing to note, in the old version, there was ahistoryObject concept, which is called in the new versionnavigator 。

Let’s look at what the new version of the Router does.

react-router/index.tsx

function Router({basename,children,location:locationProp,navigator}){
  /* Create navigationContext to hold basename, navigator, etc. * /
  let navigationContext = React.useMemo(
    () = > ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );
  /* Struct the state inside the location */
  const { pathname, search, hash, state, key } = locationProp
  /* Create a locationContext object that holds pathName, state, etc. * /
  let location = React.useMemo(() = > {
    /* .... */
     return { pathname, search, hash, state, key  }
  },[basename, pathname, search, hash, state, key])
  /* Pass navigationContext and locationContext, respectively, through context
   return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location.navigationType}} / >
    </NavigationContext.Provider>)}Copy the code

The Router plays the following roles in the new routing:

  • The navigator object and the location object of routing information are derived from useMemo. Pass them through the React Context.
  • When the route changes, theBrowserRouterChange the location via useState, so when the location changes,LocationContextChanges, the consuming LocationContext is updated.

3 Principles In depth, use the Routes and branch concepts

Using BrowserRouter as an example, we showed what the outer container does. Let’s dig deeper and take a look at what happens inside Routes and how the routing hierarchy is formed. And routing to the process of rendering the corresponding page.

Take the following examples for reference:

<Routes>
   <Route element={<Home />} path="/home" />
   <Route element={<List/>}  path="/list" />
   <Route element={<Layout/>} path="/children" >
      <Route element={<Child1/>} path="/children/child1" />
      <Route element={<Child2/>} path="/children/child2" />
   </Route>
</Routes>
Copy the code

Let’s think about it with two questions.

  • If the currentpathname δΈΊ /home, so how does the entire route show the Home component?
  • If the route is switched to/children/child1What is the flow from page update to rendering? And how inLayoutInternally renderedChild1 。

Route and Routes form the routing structure

As mentioned above, the new Route must be used in conjunction with Routes. What does the new version of Route do when the old version is critical for matching and updating containers?

react-router/index.tsx

function Route(_props){
  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

For those of you who have just seen Route, there will be no logic, just a random cue. This may surprise many students, but the Route component is not a regular component and can be understood as an empty function.




In fact, the source of all processing is the Routes component. Its function is to match the correct render branch branch based on the route changes.



Then Routes are the focus of our research.

Routes and useRoutes

Let’s start with the implementation of Routes:

react-router/index.tsx

export function Routes({children,location }) {
  return useRoutes(createRoutesFromChildren(children), location);
}
Copy the code
  • use<Routes />When useRoutes returns the React Element object, useRoutes is used as a viewhooks. Routes essentially uses useRoutes.

If you can use useRoutes, you can change the route configuration structure directly to an element structure, and you are responsible for displaying the routing components that match the route, then useRoutes is the core of the routing architecture.

So before we get to useRoutes let’s figure out what does createRoutesFromChildren do?

react-router/index.tsx -> createRoutesFromChildren

function createRoutesFromChildren(children) { /* From a hierarchy nested structure */
  let routes = [];
  Children.forEach(children, element= > {
    /* Omit element validation, and flmanagement processing logic */
    let route = {
      caseSensitive: element.props.caseSensitive,  // Case sensitive
      element: element.props.element,              / / element object
      index: element.props.index,                  / / index index
      path: element.props.path                     // Route path
    };
    if (element.props.children) {
      route.children = createRoutesFromChildren(element.props.children);
    }
    routes.push(route);
  });
  return routes;
}
Copy the code
  • CreateRoutesFromChildren internally assigns the Route component to the structure via react.children. ForEach, and internally calls the recursive, deeply recursive Children structure.

CreateRoutesFromChildren can turn react Element objects of type

into normal Route object structures. We’ve already said that Route is essentially an empty function and is not actually mounted, so it is transformed by createRoutesFromChildren handling.

For example, the following structure:

<Routes>
   <Route element={<Home />} path="/home" />
   <Route element={<List/>}  path="/list" />
   <Route element={<Layout/>} path="/children" >
      <Route element={<Child1/>} path="/children/child1" />
      <Route element={<Child2/>} path="/children/child2" />
   </Route>
</Routes>
Copy the code

Element is converted to the following structure:

The next thing exposed is useRoute, which seems to be involved in everything from routing mount to switching routes for re-rendering. So let’s focus on the custom hooks.

react-router/useRoutes

function useRoutes(routes, locationArg) {

    let locationFromContext = useLocation();
   / *TODO:Stage 1: Calculate pathName */
   / /... The code is omitted

   / *TODO:Phase 2: Find the matching route branch */
  let matches = matchRoutes(routes, {
    pathname: remainingPathname
  });
  console.log('----match-----',matches)

  / *TODO:Stage 3: Render the corresponding routing component */
  return _renderMatches(matches && matches.map(match= > Object.assign({}, match, {
    params: Object.assign({}, parentParams, match.params),
    pathname: joinPaths([parentPathnameBase, match.pathname]),
    pathnameBase: match.pathnameBase === "/" ? parentPathnameBase : joinPaths([parentPathnameBase, match.pathnameBase])
  })), parentMatches);
}
Copy the code

This code is part of the v6 routing core, which I’ve broken down into three stages to help you understand.

  • In the first stage, the corresponding Pathname is generatedThe preceding example is used again, for example, route switchover/children/child1, so pathname is/children/child1.
  • Phase two, passmatchRoutesTo find the matching route branch.What is a matching route branch, such as switching routes to/children/child1Root -> children -> child1. Let’s print matches and look at the data structure.

  • The other thing is thatuseRoutesInternal useuseLocation. When the Location object changes, useRoutes will re-render.

Matches is the flattened route structure. It is an array structure. Therefore, index 0 is the layer 1 route and index 1 is the layer 2 route. So let’s look at the implementation of matchRoutes.

MatchRoutes and _renderMatches render route branches

react-router/index.tsx -> matchRoutes

function matchRoutes(routes,locationArg,basename){
    /* Flat routes structure */
    let branches = flattenRoutes(routes);
    /* Sort route */
    rankRouteBranches(branches);
    let matches = null;
    /* Through matchRouteBranch */
    for (let i = 0; matches == null && i < branches.length; ++i) {
      matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
Copy the code
  • The first flattening of the array on the grounder was done on routes. The flattening looked something like this.

There is a routesMeta attribute in flattened branches, storing the information of each route. For example, /children/child1 contains two layers of routes in essence. The first layer is /children and the second layer is /child1;

  • Then adjust the order of routes by rankRouteBranches.

  • Finally, the for loop and matchRouteBranch find the route branch to render, terminating the loop if matches is not null. The principle of matchRouteBranch is not covered for space reasons, but its main function is to find routes under the routesMeta to be rendered by pathname. The final matches structure is then formed.

Matches, which we know holds the route to be rendered. Then the next step is to render the route, render the corresponding page. So that’s basically what _renderMatches does, so what does this function do?

react-router/index.tsx -> _renderMatches

function _renderMatches(matches, parentMatches) {
  if (parentMatches === void 0) {
    parentMatches = [];
  }
  if (matches == null) return null;
  return matches.reduceRight((outlet, match, index) = > {
    /* Use the element of the previous item as an outlet to the next item */
    return  createElement(RouteContext.Provider, {
      children: match.route.element ! = =undefined ? match.route.element : /*#__PURE__*/createElement(Outlet, null),
      value: {
        outlet,
        matches: parentMatches.concat(matches.slice(0, index + 1))}}); },null);
}
Copy the code

This code is very subtle and contains a large amount of information. It forms the React structure Elmenet through reduceRight, which solves three problems:

    1. The first layer of route page is rendered.
    1. How outlets are rendered as child routes.
    1. How the routing state is transmitted.

First of all, we know that reduceRight is traversed from right to left. As mentioned before, the match structure is root -> children -> child1. ReduceRight takes the content returned by the former item as an outlet of the latter item. So the match structure above will be treated like this.

  • 1 first wraps child1 with the provider, so that the content that child1 really needs to render will be treated as the children of the provider. Finally, the current provider will be returned to Child1. So the first level outlet is null.
  • 2 Then the provider returned by the first layer, as an outlet to the second layer, is passed through the outlet property in the value of the second layer provider. The Layout component is then returned as children.
  • Child1 is not rendered directly, but is passed as a Provider property.

So child1 is rendered as an Outlet placeholder in a Container. So let’s think about what outlets do, we’ll probably get our Outlet from the first layer provider using useContext and then we’ll render the provider for Child1, Child1 is rendered as children. Let’s see if this is true.

react-router/index.tsx -> Outlet

export function Outlet(props: OutletProps) :React.ReactElement | null {
  return useOutlet(props.context);
} 
Copy the code
  • An Outlet is essentially using useOutlet, so let’s seeuseOutlet.

react-router/index.tsx -> useOutlet

export function useOutlet(context? : unknown) :React.ReactElement | null {
  let outlet = React.useContext(RouteContext).outlet;
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}
Copy the code
  • Get the outlet above the Provider, and render the outlet, so the secondary subpath will render normally.

At this point, the entire V6 rendering philosophy is clear.

What we do in reduceRight is represented by a flow chart.

Route updates to the corresponding component rendering presentation flow

Let’s look at what happens if a jump is made from navigator, such as a home jump to a child1 component.

  • Using BrowserRouter as an example, when a route is updated, the Listen event in BrowserRouter will be triggered first, and a new Location object will be formed. And then the locationContext is updated.
  • UseRoutes consumes the locationContext internally, and changes to the locationContext cause useRoutes to re-execute.
  • The useRoutes reexecutes, internally calling matchRoutes and _renderMatches to find the new render branch and render the corresponding page.

The entire rendering process is relatively simple and clear.

The difference between V5 and V6

What is the difference between V6 and V5?

At the component level:

  • The Router Switch Route structure is used in the routing of the old version. Router -> transfer status is responsible for sending updates. Switch -> matches a unique route; Route -> Real Render routing components.
  • The new version uses the Router Routes Route structure. The Router extracts a context. Routes -> create route render branch, render route; Instead of rendering actual routes, routes form a branching structure.

On the level of use:

  • For nested routes of old versions, configure level-2 routes and write them in specific service components.
  • In the new version of routing, the routing structure is unified in the outer layer, the routing structure is clearer, and the rendering of descendant routes is implemented through outlets, somewhat similar to vUEview-router.
  • The new version has made major changes to the API, such as changing useHistory to useNavigate, reducing some apis and adding some new ones.

On the principle level:

  • The essence of routing in older versions was the Route component, which rerenders when the routing context changes and then matches to determine whether the business component is rendered or not.
  • The routing essence of the new version lies in the Routes component. When the location context changes, Routes are re-rendered to re-form the rendering branch, and then outlets are passed layer by layer via provider to match rendering.

Five summarizes

This article mainly introduces the basic use of V6, principle introduction, and V5 difference, interested friends can try to use V6. The overall feeling is pretty good.

The resources

  • Upgrading from v5
  • The react-Router routing principle was thoroughly understood