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

Tyler Smith - Oct 29 '19 - - Dev Community

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player