Norayr Ghukasyan
Norayr Ghukasyan

Reputation: 1408

A Route is only ever to be used as the child of <Routes> element, in React router v6

I am struggling to render my routes list with React router v6. I have already read the docs and I am aware of a new structure of routing, but currently having a problem rendering list (nested) of routes.

export default [{
  path: '/',
  element: Root,
  routes: [{
    path: REGISTER,
    element: Register,
    isProtected: true,
  }],
}];

export const RouteMatcher = ({ routes }) => {
  return routes.map((route, index) => {
    if (route.element) {
      return (
        <Route key={index} path={route.path} element={<route.element />}>
          {route.routes ? (
            <Route path={route.path} element={<RouteMatcher routes={route.routes} />
          ) : null}
        </Route>
      );
    }

    return null;
  });
};

<BrowserRouter>
  <Routes>
    <Route path="/" element={<RouteMatcher routes={routes} />} />
  </Routes>
</BrowserRouter>

It's not obvious to me what's going on here even though the error message has a clear explanation of the issue. Even I try this way it doesn't work.

 <BrowserRouter>
      <Routes>
        <Route path="/">
          {routes.map((route, index) => {
            if (route.element) {
              return (
                <Route key={index} path={route.path} element={<route.element />}>
                  {route.routes ? (
                    <Route path={route.path} element={<RouteMatcher routes={route.routes} indexPathname={indexPathname} />} />
                  ) : null}
                </Route>
              );
            }

            return null;
          })}
        </Route>
      </Routes>
    </BrowserRouter>

As you can see Route is always a child of Routes or Route (when nested).

** UPDATED ** Here is the react router v5 implementation

<Provider store={store}>
   <GlobalStyles />
   <AppContentWrapper>
       <RouteHandler />
   </AppContentWrapper>
</Provider>

Route handler component

<BrowserRouter>
   {generateRouteMatches(
       routes,
       indexPathname,
       auth.isLoading,
       auth.isLoggedIn
    )}
</BrowserRouter>

Route generator component

export const generateRouteMatches = (
  baseRoutes: IAppRoute[],
  indexPathname: string,
  userPermissions: PermissionShape[],
  isLoading: boolean,
  isLoggedIn: boolean,
) => {
  AccessControl.createPermissions(userPermissions);

  return baseRoutes.map((route) => (
    <MatchRoutes
      indexPathname={indexPathname}
      authIsLoading={isLoading}
      isLoggedIn={isLoggedIn}
      key={route.path}
      {...route}
    />
  ));
};

MatchRoutes component with RouteRenderer

function MatchRoutes({ location, ...route }: any) {
  const routePermissions = AccessControl.getPermissions(route.zone);

  if (!routePermissions.canRead && route.isProtectedRoute) {
    return <Route {...omit(route, ['component'])} component={() => {
      return <div className="centerXY">You dont have permissions to view this page</div>;
    }} />;
  }

  return (
    <Route {...omit(route, ['component'])} render={(props) => (
      <RouteRenderer {...props} route={route} location={location} />
    )} />
  );
}

function RouteRenderer({ route, ...props }: any) {
  const location = useLocation();

  if (location?.pathname === '/') {
    return (
      <Redirect
        to={{
          pathname: route.indexPathname,
          state: { from: location },
        }}
      />
    );
  }

  if (route.isProtectedRoute && !route.isLoggedIn && !route.authIsLoading) {
    return (
      <Redirect to={{
        pathname: '/login',
        state: { from: location },
      }}/>
    );
  }

  if (route.component) {
    return (
      <route.component
        {...props}
        params={props.match.params}
        routes={route.routes}
      >
        {route.routes
          ? route.routes.map((cRoute, idx) => (
            <MatchRoutes
              authIsLoading={route.authIsLoading}
              isLoggedIn={route.isLoggedIn}
              key={idx}
              {...cRoute}
            />
          ))
          : null
        }
      </route.component>
    );
  } else if (route.routes) {
    return (
      <>
        {route.routes.map((cRoute, idx) => (
          <MatchRoutes
            authIsLoading={route.authIsLoading}
            isLoggedIn={route.isLoggedIn}
            key={idx}
            {...cRoute}
          />
        ))}
      </>
    );
  } else {
    return null;
  }

}

export default MatchRoutes;

Upvotes: 0

Views: 897

Answers (1)

Drew Reese
Drew Reese

Reputation: 202608

In the first example the RouteMatcher component is rendering a Route component directly. The mapped routes it is rendering need to be wrapped in a Routes component.

export const RouteMatcher = ({ routes }) => {
  return (
    <Routes>
      {routes
        .filter(route => route.element)
        .map((route, index) => {
          return (
            <Route
              key={index}
              path={route.path}
              element={<route.element />}
            >
              {route.routes && (
                <Route
                  path={route.path}
                  element={<RouteMatcher routes={route.routes} />}
                />
              )}
            </Route>
          );
        })
      }
    </Routes>
  );
};

I suspect something similar is occurring int he second code example as well.

I suggest using a better formed routes configuration and use the useRoutes hook.

Example:

export default [{
  path: '/',
  element: <Root />,
  children: [
    {
      element: <AuthOutlet />,
      children: [
        {
          path: REGISTER,
          element: <Register />,
        },
        ... other routes to protect ...
      ],
      ... other unprotected routes ...
    },
  ],
}];

...

import appRoutes from '../path/to/routes';

...

const routes = useRoutes(appRoutes);

...

<BrowserRouter>
  {routes}
</BrowserRouter>

Upvotes: 1

Related Questions