React Router v6 and recursive rendering of Route component from data

Viewed 353

Following up from my question React router v6 and relative links from page within route, I'm trying to refactor the routes in our app to be more nested.

Trouble is that it doesn't seem possible to render a Route element recursively from data, because react-router insists that Route is directly inside Route and not wrapped in another component, and I cannot see how to render recursively (to arbitrary depth) any other way.

Reproduction on codesandbox.

import React from "react";
import { BrowserRouter, Routes, Route} from "react-router-dom";

import "./styles.css";

function GenericPage() {
  return <div className="page">Generic page</div>;
}

const nav = {
  slug: "",
  title: "Home",
  children: [
    {
      slug: "foo",
      title: "Foo"
    },
    {
      slug: "bar",
      title: "Bar"
    }
  ]
};

const RecursiveRoute = ({ node }) => {
  return (
    <Route path={node.slug} element={<GenericPage />}>
      {node.children?.map((child) => (
        <RecursiveRoute node={child} />
      ))}
    </Route>
  );
};

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <RecursiveRoute node={nav} />
      </Routes>
    </BrowserRouter>
  );
}

Error from react-router:

[RecursiveRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
2 Answers

Issue

As the error indicates, you can't render Route components directly, they must be rendered directly by a Routes component, or another Route component in the case of nested routes.

Solution

Refactor RecursiveRoute to render a Routes component with a route for the current node and then map the node's children to routes that render the RecursiveRoute as an element.

Example:

function GenericPage({ title }) {
  return (
    <div className="page">
      {title} page
    </div>
  );
}

const RecursiveRoute = ({ node }) => (
  <Routes>
    <Route
      path={`${node.slug}/*`}
      element={<GenericPage title={node.title} />}
    />
    {node.children?.map((child) => (
      <Route
        key={child.slug}
        element={<RecursiveRoute key={child.slug} node={child} />}
      />
    ))}
  </Routes>
);

Suggestion

I strongly suggest not trying to roll your own custom route configuration and renderer, use the useRoutes hook instead to do all the heavy lifting for you.

Example:

Refactor the navigation config:

const nav = [
  {
    path: "/",
    element: <GenericPage title="Home" />,
    children: [
      {
        path: "foo",
        element: <GenericPage title="Foo" />
      },
      {
        path: "bar",
        element: <GenericPage title="Bar" />
      }
    ]
  }
];

Pass the config to the useRoutes hook and render the result:

const routes = useRoutes(nav);

...

return routes;

Demo

Edit react-router-v6-and-recursive-rendering-of-route-component-from-data

I had the same puzzle to solve. In general I solve it by passing a function in Routes component. Here is my solution with few code snippets.

  // in Routes.ts 
  interface IRoutes {
    path: string
    component: JSX.Element
    children?: IRoutes[]
  }

  const routes: IRoutes[] = [
    {
      path: 'warehouse'
      component: <WarehousePage />
      children: [
        {
          path: 'products'
          component: <ProductsPage />
        },
        {
          path: 'units'
          component: <UnitsPage />
        },
      ]
    },
  ]
  
  // in AppRouter.tsx
  const renderRoutesRecursive = (routes: IRoutes[]) =>
    routes.map((route, index) =>
      route.children ? (
        renderRoutesRecursive(route.children)
      ) : (
        <Route
          key={index}
          path={route.path}
          element={route.component}
        />
      ),
    )

  const renderRoutes = useMemo(() => renderRoutesRecursive(routes), [routes])

  return (
    <Routes>
      <Route path='/' element={<Layout />}>
        {renderRoutes}
      </Route>
    </Routes>
  )

  // in Layout.tsx
  const Layout = () => {
    return (
      <>
        <Header />
        <Navigation />
        <Main>
          <Outlet />
        </Main>
        <Footer />
      </>
    )
  }

Related