DEV Community

Tyler Smith
Tyler Smith

Posted on

How to get React Router 4 to allow nested components inside of a Switch

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:

  1. We return an array of routes (allowed since React 16) instead of routes contained inside a fragment.
  2. 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)

Collapse
 
dosentmatter profile image
dosentmatter • Edited

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 use React.Children.forEach() instead because toArray() actually returns children with keys prefixed. The reason they do that is to preserve your statically defined Routes that probably don't have a key set on them. It is a minor optimization to prevent re-mounting for different Routes 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:

<Switch>
  {AdminRouteGroup({ contentType: "pages" })}
  {AdminRouteGroup({ contentType: "posts" })}
</Switch>
// transpiles to
React.createElement(
  Switch,
  null,
  AdminRouteGroup({ contentType: "pages" }),
  AdminRouteGroup({ contentType: "pages" }),
)
Enter fullscreen mode Exit fullscreen mode

React dev mode sees that those two AdminRouteGroup children are arrays and gives you a warning if they are missing key.

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....

React.createElement(
  Switch,
  null,
  ...AdminRouteGroup({ contentType: "pages" }),
  ...AdminRouteGroup({ contentType: "pages" }),
)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jacobsfletch profile image
Jacob Fletcher

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!

Collapse
 
tylerlwsmith profile image
Tyler Smith

Thank you for the kind words! I'm glad you found it useful: this problem had me completely stumped for hours.

Collapse
 
lam1051999 profile image
lam1051999

wow, i had the trouble with nested Fragment in Switch, your answer really really helped me. Thank you so much

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad it helped!

Collapse
 
lupi410 profile image
LV4

This made my day, thanks!

Collapse
 
tylerlwsmith profile image
Tyler Smith

I'm glad this helped! I got stuck on this problem for hours.

Collapse
 
shulashulamit profile image
Shula

Signed up for this website to upvote your post. A clean and easy solution.