react-router v6 demystified (part 3)
Tags: javascript, tutorial, react
Published: 2021-09-22 13:35:59

In the previous article, we have seen how to implement a `react-router` v6 lib like. But we have not implemented the nested `Route` and `Routes`. We are going to do this major features in this article.

## Outlet

Before going deep into nested `Route`, we need to talk about a new component. The `Outlet` represents the nested `Route` of the current one.

For example in the example:

```jsx
<Route path="hobby">
  <Route path="/" element={<HobbyListPage />} />
  <Route path=":name" element={<HobbyDetailPage />} />
</Route>
```

The `Outlet` of `<Route path="hobby">` will be in function of the url:
- `<HobbyListPage />` when on `/hobby`
- `<HobbyDetailPage />` when on `/hobby/:name`

> **Note:** Each routes are wrapped with their own `RouteContext`

### How is it stored?

Yeah you may ask: _"How is this done?"_ 
Actually it's quite easy the `outlet` is stored in the `RouteContext`.

### Implementation

The implementation of the `Outlet` component is:

```jsx
function Outlet() {
  // Get the outlet from the current `RouteContext`
  const { outlet } = useRouteContext();

  return outlet;
}
```

### Small change in `Route`

As you may notice we want to be able to do `<Route path="hobby">`. Yep, there is no element. So in this case we want the element to be by default `Outlet`:

```jsx
// Path just usefull for Routes
function Route({ path, element = <Outlet /> }) {
  return element;
}
```

And here we go, we are ready to do some nested `Route` :)

----

## Nested `Route`

In this part let's implement the ability to do:

```jsx
<Routes>
  <Route path="hobby">
    <Route path="/" element={<HobbyListPage />} />
    <Route path=":name" element={<HobbyDetailPage />} />
  </Route>
  <Route path="about" element={<AboutPage />} />
  <Route path="/" element={<HomePage />} />
</Routes>
```

As a reminder, we transform the React element into simple javascript objects, in a `buildRouteElementsFromChildren` method.

We will have to handle in this method, the potential children that can have a `Route` element.

```javascript
function buildRouteElementsFromChildren(children) {
  const routeElements = [];

  // We loop on children elements to extract the `path`
  // And make a simple array of { elenent, path }
  React.Children.forEach(children, (routeElement) => {
    // Not a valid React element, let's go next
    if (!React.isValidElement(routeElement)) {
      return;
    }

    const route = {
      // We need to keep the route to maybe display it later
      element: routeElement,
      // Let's get the path from the route props
      // If there is no path, we consider it's "/"
      path: routeElement.props.path || "/",
    };

    // If the `Route` has children it means it has nested `Route`
    if (routeElement.props.children) {
      // Let's transform the children `Route`s into objects
      // with some recursivity
      let childrenRoutes = buildRouteElementsFromChildren(
        routeElement.props.children
      );

      // It could happen that it was only 
      // non valid React elements
      if (childrenRoutes.length > 0) {
        // Notify that this route has children
        route.children = childrenRoutes;
      }
    }

    routeElements.push(route);
  });

  return routeElements;
}
```

So the previous example will become:

```javascript
[
  {
    path: "hobby",
    // It's the default element
    element: <Outlet />,
    children: [
      {
        path: "/",
        element: <HobbyListPage />,
      },
      {
        path: ":name",
        element: <HobbyDetailPage />,
      },
    ],
  },
  {
    path: "about",
    element: <AboutPage />,
  },
  {
    path: "/",
    element: <HomePage />,
  },
]
```

Ok, now that we have a simple object, we need to list all the possible paths that we will be named branches.

Let's see the process with this gif:

![Branches creation process](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63ax7e2l3nz3e6d6hbg8.gif)

The final branches are:

```javascript
[
  [
    {
      path: "hobby",
      element: <Outlet />,
    },
    {
      path: "/",
      element: <HobbyListPage />,
    },
  ],
  [
    {
      path: "hobby",
      element: <Outlet />,
    },
    {
      path: ":name",
      element: <HobbyDetailPage />,
    },
  ],
  [
    {
      path: "hobby",
      element: <Outlet />,
    },
  ],
  [
    {
      path: "about",
      element: <AboutPage />,
    },
  ],
  [
    {
      path: "/",
      element: <HomePage />,
    },
  ],
]
```

Not too complicated, isn't it?

Let's make some code:

```javascript
function createBranches(routes, parentRoutes = []) {
  const branches = [];

  routes.forEach((route) => {
    const routes = parentRoutes.concat(route);

    // If the `Route` has children, it means
    // it has nested `Route`s
    // So let's process them by recursively call
    // `createBranches` with them
    // We need to pass the current path and the parentRoutes
    if (route.children) {
      branches.push(
        ...createBranches(route.children, routes)
      );
    }

    branches.push(routes);
  });
  return branches;
}
```

And now we have to find the matching branch. The idea is the same than in the 2nd article but now we will loop on routes that can be in a branch.

The process will be:
- Loop on branches
- We instantiate a variable `pathname` with the current one (it will be changed)
- In the branch, let's loop on routes:
 - Build regexp from the root path (if it's the last route, do not forget to end with `$`)
 - If the location matches the regexp and it's not the last route we remove the matching pathname from the current one to test it with the next route. 
 - If it isn't the last route let's do the same thing with the next branch
 - If it was the last route and it has matched we found the right branch. Let's return it. Otherwise let's process the next branch.


And here is the corresponding code:

```javascript
// routes variable corresponds to a branch
function matchRoute(routes, currentPathname) {
  // Ensure that the path is ending with a /
  // This is done for easy check
  currentPathname = normalizePath(currentPathname + "/");

  let matchedPathname = "/";
  let matchedParams = {};

  const matchesRoutes = [];

  for (let i = 0; i < routes.length; i++) {
    const route = routes[i];
    const isLastRoute = i === routes.length - 1;

    const routePath = route.path;
    const currentParamsName = [];

    const regexpPath = routePath
      // Ensure there is a leading /
      .replace(/^\/*/, "/")
      .replace(/:(\w+)/g, (_, value) => {
        currentParamsName.push(value);

        return "(\\w+)";
      });
    // Maybe the location end by "/" let's include it
    const regexpValue = `^${regexpPath}\\/?${
      isLastRoute ? "$" : ""
    }`;
    const matcher = new RegExp(regexpValue);

    const pathNameTocheck = normalizePath(
      `${
        matchedPathname === "/"
          ? currentPathname
          : currentPathname.slice(matchedPathname.length)
      }/`
    );

    const matches = pathNameTocheck.match(matcher);

    // The route doesn't match
    // Let's end this
    if (!matches) {
      return null;
    }

    const [matchingPathname, ...matchValues] = matches;
    matchedPathname = joinPaths(
      matchedPathname,
      matchingPathname
    );

    const currentParams = currentParamsName.reduce(
      (acc, paramName, index) => {
        acc[paramName] = matchValues[index];
        return acc;
      },
      {}
    );

    matchedParams = { ...matchedParams, ...currentParams };

    matchesRoutes.push({
      params: matchedParams,
      route,
      path: matchedPathname,
    });
  }

  return matchesRoutes;
}
```

Now that we have found the matching branch, we need to display it. As you may have seen the parent Route is the first element of the branch so we need to `reduceRight` to pass second as outlet of previous element.

```javascript
function Routes({ children }) {
  // Construct an Array of object corresponding to 
  // available Route elements
  const routeElements =
    buildRouteElementsFromChildren(children);
  // Get the current pathname
  const { pathname: currentPathname } = useLocation();

  // We want to normalize the pahts
  // They need to start by a "/""
  normalizePathOfRouteElements(routeElements);

  // A Routes component can only have one matching Route
  const matchingRoute = findFirstMatchingRoute(
    routeElements,
    currentPathname
  );

  // No matching, let's show nothing
  if (!matchingRoute) {
    return null;
  }

  return matchingRoute.reduceRight(
    (outlet, { route, path, params }) => {
      return (
        <RouteContext.Provider
          value={{
            outlet,
            params,
            path,
          }}
        >
          {route.element}
        </RouteContext.Provider>
      );
    },
    null
  );
}
```

And that's it we have a working implementation of nested `Route`.

Let's now see how to implement nested `Routes`.

----

## Nested `Routes`

Before seeing an example of what we would like to be able to code:

```jsx
function App() {
  return (
    <Router>
      <Routes>
        <Route path="about/*" element={<AboutPage />} />
      </Routes>
    </Router>
  );
}

function AboutPage() {
  // Here you will find a nested `Routes`
  return (
    <Routes>
      <Route
        path="extra"
        element={<p>An extra element made with a Routes</p>}
      />
      <Route
        path="/"
        element={
          <Link to="extra" className="link">
            Show extra information
          </Link>
        }
      />
    </Routes>
  );
}
```

> **Note:** You may have notices the trailing `/*` which indicates that it should match all path


In the `Routes` component, we can get the parent pathname with its params, thanks to the `RouteContext`:

```javascript
const { params: parentParams, path: parentPath } =
  useContext(RouteContext);
```

And now we pass the `parentPath` to the `findFirstMatchingRoute` method:

```javascript
const matchingRoute = findFirstMatchingRoute(
  routeElements,
  currentPathname,
  parentPath
);
```

And when we put the path and params in the Context we just have to concat with the parents ones:

```jsx
return matchingRoute.reduceRight(
  (outlet, { route, path, params }) => {
    return (
      <RouteContext.Provider
        value={{
          outlet,
          // We want to have the current params 
          // and the parent's too
          params: { ...parentParams, ...params },
          path: joinPaths(parentPath, path),
        }}
      >
        {route.element}
      </RouteContext.Provider>
    );
  },
  null
);
```

The final code of `Routes` is then:

```jsx
function Routes({ children }) {
  // Construct an Array of object corresponding to available Route elements
  const routeElements =
    buildRouteElementsFromChildren(children);
  // Get the current pathname
  const { pathname: currentPathname } = useLocation();
  // Get potential Routes parent pathname
  const { params: parentParams, path: parentPath } =
    useContext(RouteContext);

  // We want to normalize the pahts
  // They need to start by a "/""
  normalizePathOfRouteElements(routeElements);

  // A Routes component can only have one matching Route
  const matchingRoute = findFirstMatchingRoute(
    routeElements,
    currentPathname,
    parentPath
  );

  // No matching, let's show nothing
  if (!matchingRoute) {
    return null;
  }

  return matchingRoute.reduceRight(
    (outlet, { route, path, params }) => {
      return (
        <RouteContext.Provider
          value={{
            outlet,
            // We want to have the current params and the parent's too
            params: { ...parentParams, ...params },
            path: joinPaths(parentPath, path),
          }}
        >
          {route.element}
        </RouteContext.Provider>
      );
    },
    null
  );
}
```

Okay it looks good, but what is the magic of `findFirstMatchingRoute`?

----

### `findFirstMatchingRoute` final implementation

In the method, we just going to remove of the `currentPathname` the parent's one.

```javascript
function findFirstMatchingRoute(
  routes,
  currentPathname,
  parentPath
) {
  const branches = createBranches(routes);

  // We remove the parentPath of the current pathname
  currentPathname = currentPathname.slice(
    parentPath.length
  );

  for (const branch of branches) {
    const result = matchRoute(branch, currentPathname);

    if (result) {
      return result;
    }
  }
  return null;
}
```

You have probably figure it out that the real magix is in the `matchRoute` function.

### `matchRoute` implementation

The changes done in the method concern the construction of the `regexpPath`. 
The major thing to understand is that when the Route path is ending with a `*` with are going to add `(.*)` to the regex to match everything after the wanted pathname.
But doing this naively will break the value of the matching pathname. For example:

```javascript
// If we have the Route path: 'hobby/:name/*'
// And the current pathname is: '/hobby/knitting/photos'

// In this case the matching pathname will be:
const matchingPathname = '/hobby/knitting/photos';

// But we would like to have
const matchingPathname = '/hobby/knitting';
```

So we are going to make a group by wrapping with parentheses before adding `(.*)`.

The construction of the regex is now:

```javascript
const regexpPath =
  "(" +
  routePath
    // Ensure there is a leading /
    .replace(/^\/*/, "/")
    // We do not want to keep ending / or /*
    .replace(/\/?\*?$/, "")
    .replace(/:(\w+)/g, (_, value) => {
      currentParamsName.push(value);

      return "(\\w+)";
    }) +
  ")";
// Maybe the location end by "/" let's include it
let regexpValue = `^${regexpPath}\\/?`;

if (routePath.endsWith("*")) {
  regexpValue += "(.*)";
  currentParamsName.push("*");
}

if (isLastRoute) {
  regexpValue += "$";
}
```

And we now get the matching pathname at the second position of the matches array:

```javascript
// With the grouping the matching pathname is now
// at the second poistiong (indice 1)
const [_, matchingPathname, ...matchValues] = matches;
```

And here we go! We have an implementation of the nested `Routes` that works :)

----

## Playground

Here is a little code sandbox of this third part of `react-router` implementation:

{% codesandbox react-router-implem-part-3-44to7 %}

----

## Conclusion

In this third article we ended with a major feature which is to be able to do nestes `Route` and `Routes`. And a working `react-router` implementation like. 
Note, that this implementation is not perfect, you will have to make sure to put the path in the right order. For example if you put the `Route` with the path `/`, it will match EVERYTHING. In the real implementation, they coded a weight system to reorder `Route` from the more restricted path to the less one.

I hope you enjoyed the articles and you now have a better idea of how the `react-router` v6 is implemented :)

----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitter](https://twitter.com/romain_trotard ) or go to my [Website](https://www.romaintrotard.com). :panda_face: