Using a route config with React Router: the right way

When nesting routes, recent versions of React Router encourage you to declare child routes directly in the component they will render into. Although it is a perfectly valid way of doing things, it gets a bit more complicated if you are defining all your routes in a separate configuration file to begin with. Documentation includes an example of such a config, but it does not cover some pretty essential use cases, such as redirects, or routes using a render function rather than a static component declaration. Let's dive in and see how to do those!

First, let's picture a basic app looking something like the following:

import React from "react";
import { Switch } from "react-router-dom";
import Home from "./components/Home";
import Hello from "./components/Hello";
import World from "./components/World";

const routes = {
home: {
path: "/"
exact: true,
component: Home
},

hello: {
path: "/hello",
component: Hello
},

world: {
path: "/hello/world",
component: World
}
};

export default function App() {
return (
<div className="app">
<h1>Hello there!</h1>
<Switch>
{Object.entries(routes).map(([key, route]) => <Route key={key} {...route} />)}
</Switch>
</div>
);
}

Now, imagine we want our World component to actually be rendered within the Hello component, which would better reflect the URL it is accessed at and allow it to share part of its newfound parent's markup, but we want to keep all routes declarations in our config, like so:

// ...

const routes = {
home: {
path: "/"
exact: true,
component: Home
},

hello: {
path: "/hello",
component: Hello,

routes: {
world: {
path: "/hello/world",
component: World
}
}
}
};

// ...

To make this work, we need the routes object to be passed down to our Hello component, which we can achieve by superseding the standard Route component:

import React from "react";
import { Route } from "react-router-dom";

export function RecursiveRoute(route) {
return (
<Route
path={route.path}
exact={!!route.exact}
render={props => (
<route.component {...props} routes={route.routes} />
)}
/>
);
}

Then, after making use of this in our rendering logic:

import React from "react";
import { Switch } from "react-router-dom";
import RecursiveRoute from "./components/RecursiveRoute";
// ...

export default function App() {
return (
<div className="app">
<h1>Hello there!</h1>
<Switch>
{Object.entries(routes).map(([key, route]) => <RecursiveRoute key={key} {...route} />)}
</Switch>
</div>
);
}

We can do the same in Hello.js:

import React from "react";
import { Switch, Route } from "react-router-dom";
import RecursiveRoute from "./RecursiveRoute";

export default function Hello({ routes }) {
return (
<div>
<div class="sidebar"><!-- ... --></div>
<Switch>
{Object.entries(routes).map(([key, route]) => <RecursiveRoute key={key} {...route} />)}
<Route>I only appear if no child route matched</Route>
</Switch>
</div>
);
}

Routes can now be nested on an infinite number of levels in our config!

So far, we're on par with the example from the React Router docs; but what if instead of showing default content when none of our child routes match, we wanted to redirect to one of them?

// ...

const routes = {
home: {
path: "/"
exact: true,
component: Home
},

hello: {
path: "/hello",
component: Hello,
redirect: "/hello/world",

routes: {
world: {
path: "/hello/world",
component: World
}
}
}
};

// ...

Let's move that Object.entries(routes).map call to a separate function and make it better:

import { Redirect } from "react-router-dom"
import RecursiveRoute from "./components/RecursiveRoute";

export default function renderRoutes(routes) {
return Object.entries(routes).reduce((routes, [key, route]) => {
if (route.redirect) {
routes.push(<Redirect key={`${key}_redirect`} exact from={route.path} to={route.redirect} />);
}

return routes.concat(<RecursiveRoute key={key} {...route} />);
}, []);
}

Last but not least, if we need to support routes relying on render rather than component, all we have to do is to enhance RecursiveRoute a bit:

import React from "react";
import { Route } from "react-router-dom";

export function RecursiveRoute(route) {
return (
<Route
path={route.path}
exact={!!route.exact}
render={route.component
// Render given component with extra "routes" prop
? props => (
<route.component {...props} routes={route.routes} />
)

// Run given render function and inject extra "routes" prop
: props => {
const rendered = route.render(props);

return {
...rendered,
props: {
...rendered.props,
routes: route.routes
}
};
}
}
/>
);
}

This allows us to keep our routing logic fully contained in our config.