1
votes

I am implementing Authenticated Routes with Typescript and React using the render props of the Route component from React Router v4.

Routes:

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ROUTES } from 'utils/constants';
import HomePage from 'components/pages/Home';
import GuestLogin from 'components/pages/GuestLogin';
import ProfilePage from 'components/pages/Profile';
import NotFoundPage from 'components/pages/NotFound';
import ResetPassword from 'components/pages/ResetPassword';
import SetPassword from 'components/pages/SetPassword';
import LoginContainer from 'containers/Login';
import PrivateRoute from './PrivateRoute';

const Routes: React.FunctionComponent = () => (
  <Switch>
    <Route path={ROUTES.LOGIN} component={LoginContainer} exact></Route>
    <PrivateRoute
      path={ROUTES.HOME}
      component={HomePage}
    ></PrivateRoute>
    <Route path={ROUTES.GUEST_LOGIN} component={GuestLogin}></Route>
    <Route path={ROUTES.RESET_PASSWORD} component={ResetPassword}></Route>
    <Route path={ROUTES.SET_PASSWORD} component={SetPassword}></Route>
    <Route path={ROUTES.PROFILE} component={ProfilePage}></Route>
    <Route component={NotFoundPage}></Route>
  </Switch>
);

export default Routes;

Private Route:

import React from 'react';    
import { useAppContext } from 'containers/App/AppContext';
import { RouteProps, Route, Redirect } from 'react-router-dom';
import { ROUTES } from 'utils/constants';

const PrivateRoute: React.FunctionComponent<RouteProps> = ({
  component: Component,
  ...routeProps
}) => {
  const { isSignedIn } = useAppContext();
  const ComponentToRender = Component as React.ElementType;
  return (
    <Route
      {...routeProps}
      render={(props) =>
        isSignedIn ? (
          <ComponentToRender {...props} />
        ) : (
          <Redirect to={ROUTES.LOGIN} />
        )
      }
    />
  );
};

export default PrivateRoute;

The problem is that I want to call the Component set on the props, however, every time I try this, Typescript throws the following error.

JSX element type 'Component' does not have any construct or call signatures.  TS2604

Image of the error

The reason seems to be that the component type for the Route is not the one Typescript expects as explained here: https://github.com/microsoft/TypeScript/issues/28631, therefore I just created a copy which has a new type(ComponentToRender).

Is there a better way to implement this? Maybe overwriting the RouteProps component element?

Thanks!

1
Which version of react and @types/react are you using?tmhao2005
Hi, I am using React 16.13.1 and @types/react 16.9.41.juanjo12x
Since I don't see anything wrong with your code so can you share your stack trace which shows where the error come from?tmhao2005
I updated the post with an image of the actual error.juanjo12x
No worries, I will add a simple repo later if I don't find a solution. Thanks!juanjo12x

1 Answers

4
votes

I finally understood the error and solved it! The key was that the component attribute and render function are handled differently type-wise. By using RouteProps , the Typescript compiler is actually setting the component props as ComponentType (the type specified for 'component' according to RouterProps), which cannot be used for the render function, as seem in the index.d.ts file.

export interface RouteProps {
    location?: H.Location;
    component?: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
    render?: (props: RouteComponentProps<any>) => React.ReactNode;
    children?: ((props: RouteChildrenProps<any>) => React.ReactNode) | React.ReactNode;
    path?: string | string[];
    exact?: boolean;
    sensitive?: boolean;
    strict?: boolean;
}

This behaviour becomes apparent just by moving the render function into the Routes file and not using the PrivateRoute component. The code below works as expected, because the compiler infers the type correctly (React.ReactNode).

<Route
  path={ROUTES.POINTS_HISTORY}
  render={(props) =>
    isSignedIn ? <PointsTransactions /> : <Redirect to={ROUTES.LOGIN} />
  }
></Route>

Therefore, to solve the problem, I just create a new type with only the necessary parameters for my use case.

import React from 'react';

import { useAppContext } from 'containers/App/AppContext';
import { RouteProps, Route, Redirect } from 'react-router-dom';
import { ROUTES } from 'utils/constants';

type PrivateRouteProps = {
  path: RouteProps['path'];
  component: React.ElementType;
};
const PrivateRoute: React.FunctionComponent<PrivateRouteProps> = ({
  component: Component,
  ...routeProps
}) => {
  const { isSignedIn } = useAppContext();
  return (
    <Route
      {...routeProps}
      render={(props) =>
        isSignedIn ? <Component /> : <Redirect to={ROUTES.LOGIN} />
      }
    />
  );
};

export default PrivateRoute;