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>
);
};