Przekaż właściwości komponentów w Private Route z Typescript i React

Aug 18 2020

Wdrażam uwierzytelnione trasy z Typescript i React przy użyciu właściwości renderowania komponentu Route z React Router v4.

Trasy:

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;

Prywatna trasa:

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;

Problem polega na tym, że chcę wywołać komponent ustawiony w rekwizytach, jednak za każdym razem, gdy próbuję, Typescript generuje następujący błąd.

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

Obraz błędu

Powodem wydaje się być to, że typ komponentu trasy nie jest tym, którego oczekuje Typescript, jak wyjaśniono tutaj: https://github.com/microsoft/TypeScript/issues/28631dlatego właśnie utworzyłem kopię, która ma nowy typ (ComponentToRender).

Czy jest lepszy sposób na wdrożenie tego? Może nadpisanie elementu składnika RouteProps?

Dzięki!

Odpowiedzi

juanjo12x Aug 25 2020 at 16:18

W końcu zrozumiałem błąd i rozwiązałem go! Kluczem było to, że atrybut komponentu i funkcja renderowania są obsługiwane różnymi typami. Używając RouteProps, kompilator Typescript w rzeczywistości ustawia właściwości komponentu jako ComponentType (typ określony dla 'komponentu' zgodnie z RouterProps), którego nie można użyć do funkcji renderowania, jak wydaje się w pliku 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;
}

To zachowanie staje się widoczne po prostu przenosząc funkcję renderowania do pliku Routes i nie korzystając ze składnika PrivateRoute. Poniższy kod działa zgodnie z oczekiwaniami, ponieważ kompilator poprawnie wywnioskuje typ (React.ReactNode).

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

Dlatego, aby rozwiązać problem, po prostu tworzę nowy typ z tylko niezbędnymi parametrami dla mojego przypadku użycia.

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;