Using a route config with React Router: the right way
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.