Home > Enterprise >  React/Typescript conditional routing - Updating from React Router Dom v5 to v6
React/Typescript conditional routing - Updating from React Router Dom v5 to v6

Time:06-17

I'm a newbie to Typescript and I'm updating our app's react router version from v5 to v6. I'm stuck on how to update this conditional routing wrapper:

// v5
import React from "react";
import {
  Redirect,
  RedirectProps,
  Route,
  RouteComponentProps,
  RouteProps,
} from "react-router-dom";

export type ConditionalRouteProps = React.FC<
  RouteProps &
    Partial<RedirectProps> & {
      condition?: boolean;
    }
>;

const ConditionalRoute: ConditionalRouteProps = ({
  component: Component,
  condition = false,
  children,
  ...rest
}) => {
  const toRender = (props: RouteComponentProps): React.ReactElement | null => {
    if (!condition) return <Redirect to={REDIRECT_DESTINATION} />;
    if (Component) return <Component {...props} />;
    return <>{children}</>;
  };

  return <Route {...rest} render={toRender} />;
};

export { ConditionalRoute };

I know the render method and Redirect component is scrapped, this is what I've done so far (WIP):

//v6
import React from "react";
import { Navigate, NavigateProps, Route, RouteProps } from "react-router-dom";

export type ConditionalRouteProps = React.FC<
  RouteProps &
    Partial<NavigateProps> & {
      condition?: boolean;
    }
>;

const ConditionalRoute: ConditionalRouteProps = ({
  element: Element,
  condition = false,
  children,
  ...rest
}) => {
  return (

<Route {...rest}>
  {!condition ? (
    <Navigate to={REDIRECT_DESTINATION} />
  ) : Element ? (
    <Element />
  ) : (
    <>{children}</>
  )}
</Route>
  );
};

export { ConditionalRoute };

and this is the error I'm getting on <Element />: JSX element type 'Element' does not have any construct or call signatures.ts(2604)

This is how to wrapper is being called:

const AdminRoute: ConditionalRouteProps = (props) => {
  const { isAdmin } = useContext(UserContext);
  return <ConditionalRoute condition={isAdmin} {...props} />;
};

...

const TestPage: React.FC = () => {
  return (
    <>
      <Link to="/admin" data-testid="navigateToAdmin">
        Navigate to admin
      </Link>
    </>
  );
};

const TestRoutes: React.FC = () => {
  return (
    <Routes>
      <Route
        path="/admin"
        element={<AdminRoute element={() => <h1>Admin Page</h1>} />}
      />
      <Route path="/" element={<TestPage />} />
    </Routes>
  );
};

describe("<AdminRoute />", () => {
  it("should let admin users in", () => {
    const { history } = renderWithProviders(<TestRoutes />, {
      providerProps: { userProviderProps: { isAdmin: true } },
    });

  userEvent.click(screen.getByTestId("navigateToAdmin"));
  expect(history.location.pathname).toEqual("/admin");
  });

  it("should redirect everyone else", () => {
    const { history } = renderWithProviders(<TestRoutes />);

    userEvent.click(screen.getByTestId("navigateToAdmin"));

    expect(screen.queryByText("Admin Page")).not.toBeInTheDocument();
    expect(history.location.pathname).toEqual("/");
  });
});

CodePudding user response:

I think you are complicating the route protection a bit. react-router-dom@6 route rendering works quite differently in v6, we don't directly render a Route in our components, they are to be rendered into the Routes component or directly nested under other Route components for route nesting.

I suggest the following implementation:

//v6
import React from "react";
import { Navigate, Outlet } from "react-router-dom";

interface IConditionalRoute = {
  condition: boolean;
};

const ConditionalRoute = ({ condition = false }: IConditionalRoute) => {
  return condition ? <Outlet /> : <Navigate to={REDIRECT_DESTINATION} />;
};

export { ConditionalRoute };

Note: I'm not super strong in Typescript, so I might not have got the syntax completely correct, but I expect the interface/code should get you close to what you would need.

Usage:

The AdminRoute would be rendered on what is called a layout route, a route that renders an Outlet and wraps other Route components. Omit the path prop so the path of the nested routes is what is used for matching/rendering.

const AdminRoute: ConditionalRouteProps = () => {
  const { isAdmin } = useContext(UserContext);
  return <ConditionalRoute condition={isAdmin} />;
};

const TestRoutes: React.FC = () => {
  return (
    <Routes>
      <Route element={<AdminRoute />}>
        <Route path="/admin" element={<h1>Admin Page</h1>} />
      </Route>
      <Route path="/" element={<TestPage />} />
    </Routes>
  );
};
  • Related