Passa gli oggetti di scena in Percorso privato con Typescript e React

Aug 18 2020

Sto implementando percorsi autenticati con Typescript e React utilizzando i props di rendering del componente Route da React Router v4.

Itinerari:

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;

Percorso privato:

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;

Il problema è che voglio chiamare il Component impostato sugli oggetti di scena, tuttavia, ogni volta che provo questo, Typescript genera il seguente errore.

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

Immagine dell'errore

Il motivo sembra essere che il tipo di componente per il percorso non è quello previsto da Typescript come spiegato qui: https://github.com/microsoft/TypeScript/issues/28631, quindi ho appena creato una copia che ha un nuovo tipo (ComponentToRender).

C'è un modo migliore per implementarlo? Forse sovrascrivendo l'elemento componente RouteProps?

Grazie!

Risposte

juanjo12x Aug 25 2020 at 16:18

Finalmente ho capito l'errore e l'ho risolto! La chiave era che l'attributo del componente e la funzione di rendering sono gestiti in modo diverso dal punto di vista del tipo. Usando RouteProps, il compilatore Typescript sta effettivamente impostando i props del componente come ComponentType (il tipo specificato per 'component' secondo RouterProps), che non può essere usato per la funzione di rendering, come sembra nel file 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;
}

Questo comportamento diventa evidente semplicemente spostando la funzione di rendering nel file Routes e non utilizzando il componente PrivateRoute. Il codice seguente funziona come previsto, poiché il compilatore deduce il tipo correttamente (React.ReactNode).

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

Pertanto, per risolvere il problema, creo semplicemente un nuovo tipo con solo i parametri necessari per il mio caso d'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;