React App with Nested Routes and Breadcrumbs
Tags: react, routing, webdev, tutorial
Published: 2020-05-17 10:24:23

---
title: React App with Nested Routes and Breadcrumbs
published: true
description:
tags: react, routing, webdev, tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/p1n191ncbmcvi47w4cnx.jpg
---

It was surprising to me to discover the absence of an adequate example of a React application with nested routes, automatically generated navigation, and breadcrumbs. All of the examples I could find require copypasting code to some extent. I'll try to fill in this gap and create an application satisfying the following criteria:

* routing with `react-router-dom`
* configurable nested routes
* automatically generated navigation and breadcrumbs
* DRY

The working example is available on GitHub: https://github.com/sneas/react-nested-routes-example

# Routes

The most obvious way to build routes is to directly put them into the markup:

```html
<Router>
  <Route path="/about">
    <About />
  </Route>
  <Route path="/users">
    <Users />
  </Route>
  <Route path="/">
    <Home />
  </Route>
</Router>
```

It's also possible to store routes in an array and render them in a loop.

```jsx
const routes = [
  {
    path: "/about",
    component: About
  },
  {
    path: "/users",
    component: Users
  },
  {
    path: "/",
    component: Home
  }
];

return (
  <Router>
    {routes.map(route => (
      <Route path={route.path} component={route.component} />
    ))}
  </Router>
);
```

Let's take this into consideration to build a router with a nested structure.

```js
const routes = [
  {
    path: "/",
    component: Home,
    routes: [
      {
        path: "/about",
        component: About,
        routes: [
          {
            path: "/about/our-team",
            component: OurTeam
          }
        ]
      },
      {
        path: "/users",
        component: Users
      },
    ]
  }
];
```

Now we need to loop through the nested structure to output all the routes. This can be achieved by flattening our tree structure.

```jsx
const flattenRoutes = routes =>
  routes
    .map(route => [route.routes ? flattenRoutes(route.routes) : [], route])
    .flat(Infinity);

const routes = [
  // Same as in previous snippet
];

return (
  <Router>
    {flattenRoutes(routes).map(route => (
      <Route path={route.path} component={route.component} />
    ))}
  </Router>
);
```

It's worth noting that `flattenRoutes` puts more specific routes closer to the beginning of the array:

1. `/about/our-team`
1. `/about`
1. `/users`
1. `/`

This will help us to use the parent route as a fallback when a child route can't be found. For example, opening `/about/non-existing-page` will end up routing user to `/about` component.

Now let's DRY things up a little bit and automatically generate prefix for each individual route based on its parent. Instead of `"/about/our-teams"` we will only need to store `"/our-teams"`.

```jsx
const combinePaths = (parent, child) =>
  `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;

const buildPaths = (navigation, parentPath = "") =>
  navigation.map(route => {
    const path = combinePaths(parentPath, route.path);

    return {
      ...route,
      path,
      ...(route.routes && { routes: buildPaths(route.routes, path) })
    };
  });

const routes = [
  {
    path: "/",
    component: Home,
    routes: [
      {
        path: "/about",
        component: About,
        routes: [
          {
            path: "/our-team",
            component: OurTeam
          }
        ]
      },
      {
        path: "/users",
        component: Users
      },
    ]
  }
];

const flattenRoutes = routes =>
  routes
    .map(route => [route.routes ? flattenRoutes(route.routes) : [], route])
    .flat(Infinity);

return (
  <Router>
    {flattenRoutes(buildPaths(routes)).map(route => (
      <Route path={route.path} component={route.component} />
    ))}
  </Router>
);
```

# Nested Menu

Let's create a nested menu for each page. In order for the nested menu to be visible on every page, we can create a single `Page` container. The `Page` container will hold the menu, breadcrumbs, and page contents.


```jsx
const Page = ({ route }) => {
  // Let's output only page contents for now and 
  // take care of the menu and breadcrumbs later
  const PageBody = route.component;
  return <PageBody />;
};

return (
  <Router>
    {flattenRoutes(buildPaths(routes)).map(route => (
      {routes.map(route => (
        <Route key={route.path} path={route.path}>
          <Page route={route} />
        </Route>
      ))}
    ))}
  </Router>
);
```

`Page` container receives the current `route` prop. This prop will be used to build a nested menu and breadcrumbs.

The nested menu for a particular page consists of menus of its parents up to the root. To build a nested menu for a particular page, each route must know its parent.

```jsx
const setupParents = (routes, parentRoute = null) =>
  routes.map(route => {
    const withParent = {
      ...route,
      ...(parentRoute && { parent: parentRoute })
    };

    return {
      ...withParent,
      ...(withParent.routes && {
        routes: setupParents(withParent.routes, withParent)
      })
    };
  });

// ...

return (
  <Router>
    {flattenRoutes(setupParents(buildPaths(routes))).map(route => (
      {routes.map(route => (
        <Route key={route.path} path={route.path}>
          <Page route={route} />
        </Route>
      ))}
    ))}
  </Router>
);
```

After parents of each page have been set, they can be used to our advantage of building nested menus.

```jsx
const Menu = ({ routes }) => (
  <nav className="menu">
    {routes.map((route, index) => (
      <NavLink key={index} to={route.path}>
        {route.label}
      </NavLink>
    ))}
  </nav>
);

const pathTo = route => {
  if (!route.parent) {
    return [route];
  }

  return [...pathTo(route.parent), route];
};

const NestedMenu = ({ route }) => (
  <>
    {pathTo(route)
      .filter(r => r.routes)
      .map((r, index) => (
        <Menu key={index} routes={r.routes} />
      ))}
  </>
);

const Page = ({ route }) => {
  const PageBody = route.component;
  return (
    <>
      <NestedMenu route={route} />
      <PageBody />
    </>
  );
};
```

We've created 2 components: `NestedMenu` and `Menu`. The `NestedMenu` component specializes in rendering the entire nested menu for a particular route. It loops through the list of parent routes from the root to the specified `route`. The list is provided by `pathTo(route)` function. The navigation for an individual route is rendered by `Menu` component.

# Breadcrumbs

For the breadcrumbs, we can use a similar approach as we used to create the nested menu.

```jsx
const Breadcrumbs = ({ route }) => (
  <nav className="breadcrumbs">
    {pathTo(route).map((crumb, index, breadcrumbs) => (
      <div key={index} className="item">
        {index < breadcrumbs.length - 1 && (
          <NavLink to={crumb.path}>{crumb.label}</NavLink>
        )}
        {index === breadcrumbs.length - 1 && crumb.label}
      </div>
    ))}
  </nav>
);

const Page = ({ route }) => {
  const PageBody = route.component;
  return (
    <>
      <NestedMenu route={route} />
      {route.parent && <Breadcrumbs route={route} />}
      <PageBody />
    </>
  );
};
```

The `Breadcrumb` component also loops through the list of routes provided by the previously described `pathTo(route)` function. It makes sure the "current" route to be rendered as a text and parent routes to be rendered as a link:

```js
{index < breadcrumbs.length - 1 && (
  <NavLink to={crumb.path}>{crumb.label}</NavLink>
)}
{index === breadcrumbs.length - 1 && crumb.label}
```

We don't want to render breadcrumbs for the root route. The root route could be determined by the absence of parents: `{route.parent && <Breadcrumbs route={route} />}`.


# Conclusion

The provided solution satisfies all the previously defined criteria:

* the app uses `react-router-dom`
* the nested routes are configured as a tree-like structure
* the navigation and breadcrumbs are rendered automatically based on the configuration
* the app code and configuration does not repeat itself