Recently, I was building a site's admin section with React. The admin section used a repeated pattern for its URL structure:
- /admin/:contentType
- /admin/:contentType/new
- /admin/:contentType/:id
Because the URL structure was the same for all content types, I had hoped that I could built a component where I passed the content type in as a prop, then have the component build my routes for each content type.
Here was my unsuccessful first-attempt using fragments:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import AdminList from "../admin/list";
import AdminEdit from "../admin/edit";
import AdminNew from "../admin/new";
const AdminRouteGroup = ({ contentType }) => (
<>
<Route
exact
path={`/admin/${contentType}`}
render={routeProps => (
<AdminList contentType={contentType} {...routeProps} />
)}
/>
<Route
exact
path={`/admin/${contentType}/new`}
render={routeProps => (
<AdminNew contentType={contentType} {...routeProps} />
)}
/>
<Route
path={`/admin/${contentType}/:id`}
render={routeProps => (
<AdminEdit contentType={contentType} {...routeProps} />
)}
/>
</>
);
const App = () => (
<Router>
<Switch>
<AdminRouteGroup contentType="pages" />
<AdminRouteGroup contentType="posts" />
</Switch>
</Router>
);
export default App;
Unfortunately, this doesn't work. On GitHub, I found that React Router collaborator Tim Dorr said the following:
Switch only works with the first level of components directly under it. We can't traverse the entire tree.
Even though the AdminRouteGroup component is rendering a fragment, React Router is still confused because it's expecting a Route component to be its direct child. Instead, it's getting our component AdminRouteGroup.
We can solve this problem with a two fold approach:
- We return an array of routes (allowed since React 16) instead of routes contained inside a fragment.
- We render the component ourselves instead of returning a JSX component.
When you return an array of components, React expects you to provide a unique key for each component. To make things simple, we'll reuse our path as our key.
Here's what that looks like all together:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import AdminList from "../admin/list";
import AdminEdit from "../admin/edit";
import AdminNew from "../admin/new";
// Have AdminRouteGroup return an array of components.
const AdminRouteGroup = ({ contentType }) => [
<Route
exact
path={`/admin/${contentType}`}
key={`/admin/${contentType}`}
render={routeProps => (
<AdminList contentType={contentType} {...routeProps} />
)}
/>,
<Route
exact
path={`/admin/${contentType}/new`}
key={`/admin/${contentType}/new`}
render={routeProps => (
<AdminNew contentType={contentType} {...routeProps} />
)}
/>,
<Route
path={`/admin/${contentType}/:id`}
key={`/admin/${contentType}/:id`}
render={routeProps => (
<AdminEdit contentType={contentType} {...routeProps} />
)}
/>
];
// Render the components directly.
const App = () => (
<Router>
<Switch>
{AdminRouteGroup({ contentType: "pages" })}
{AdminRouteGroup({ contentType: "posts" })}
</Switch>
</Router>
);
export default App;
I hope this helps. Let me know if you found this useful!