Pasar accesorios de componentes en ruta privada con Typecript y React

Aug 18 2020

Estoy implementando rutas autenticadas con Typecript y React usando los accesorios de renderización del componente Route de React Router v4.

Rutas:

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;

Ruta privada:

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;

El problema es que quiero llamar al conjunto de componentes en los accesorios, sin embargo, cada vez que intento esto, Typecript arroja el siguiente error.

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

Imagen del error

La razón parece ser que el tipo de componente para la Ruta no es el que espera Typescript como se explica aquí: https://github.com/microsoft/TypeScript/issues/28631, por lo tanto, acabo de crear una copia que tiene un nuevo tipo (ComponentToRender).

¿Existe una mejor manera de implementar esto? ¿Quizás sobrescribir el elemento del componente RouteProps?

¡Gracias!

Respuestas

juanjo12x Aug 25 2020 at 16:18

¡Finalmente entendí el error y lo resolví! La clave fue que el atributo del componente y la función de renderizado se manejan de forma diferente. Al usarlo RouteProps, el compilador de TypeScript en realidad establece los accesorios del componente como ComponentType (el tipo especificado para 'componente' según RouterProps), que no se puede usar para la función de renderizado, como aparece en el archivo index.d.ts.

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

Este comportamiento se hace evidente simplemente moviendo la función de renderizado al archivo Routes y no usando el componente PrivateRoute. El siguiente código funciona como se esperaba, porque el compilador infiere el tipo correctamente (React.ReactNode).

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

Por lo tanto, para resolver el problema, simplemente creo un nuevo tipo con solo los parámetros necesarios para mi caso de uso.

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;