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!
Top comments (8)
There is a code change you can make for some optimization and to type less.
Looking at react-router code, github.com/ReactTraining/react-rou..., you can see they purposely avoid using
React.Children.toArray()
and useReact.Children.forEach()
instead becausetoArray()
actually returns children withkey
s prefixed. The reason they do that is to preserve your statically definedRoute
s that probably don't have a key set on them. It is a minor optimization to prevent re-mounting for differentRoute
s that have the same component.So if we wanted to preserve this functionality, and not have to inject any keys, how should we write our code?
This is how your current code transpiles:
React dev mode sees that those two
AdminRouteGroup
children are arrays and gives you a warning if they are missingkey
.Instead, you can spread the children out. It would be as if you typed JSX out statically and React dev mode won't complain. In general, don't do this. The warning is there for a reason. But you can do this if you know what you are doing and know that the children are stable and won't be reorganized or modified in a render update. See Dan Abramov's comment here:
github.com/facebook/react/issues/1....
I signed up for dev.to only so that I could like this post and send this comment. I needed to map over an array and return a set of routes which does not work because of the nested Fragment within the Switch. This is a verified workaround. Thank you greatly for taking the time to write this up!
Thank you for the kind words! I'm glad you found it useful: this problem had me completely stumped for hours.
wow, i had the trouble with nested Fragment in Switch, your answer really really helped me. Thank you so much
I'm glad it helped!
This made my day, thanks!
I'm glad this helped! I got stuck on this problem for hours.
Signed up for this website to upvote your post. A clean and easy solution.