Передача свойств компонента в Private Route с помощью Typescript и React

Aug 18 2020

Я реализую аутентифицированные маршруты с помощью Typescript и React, используя свойства рендеринга компонента Route из React Router v4.

Маршруты:

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;

Частный маршрут:

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;

Проблема в том, что я хочу вызвать набор компонентов для реквизита, однако каждый раз, когда я пытаюсь это сделать, Typescript выдает следующую ошибку.

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

Изображение ошибки

Причина, по-видимому, в том, что тип компонента для Route не тот, который Typescript ожидает, как объясняется здесь: https://github.com/microsoft/TypeScript/issues/28631, поэтому я только что создал копию с новым типом (ComponentToRender).

Есть ли лучший способ реализовать это? Возможно, перезапись элемента компонента RouteProps?

Благодаря!

Ответы

juanjo12x Aug 25 2020 at 16:18

Я наконец понял ошибку и решил ее! Ключевым моментом было то, что атрибут компонента и функция рендеринга обрабатываются по-разному в зависимости от типа. При использовании RoutePropsкомпилятор Typescript фактически устанавливает свойства компонента как ComponentType (тип, указанный для 'component' в соответствии с RouterProps), который не может использоваться для функции рендеринга, как это показано в файле 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;
}

Такое поведение становится очевидным, если просто переместить функцию рендеринга в файл Routes, а не использовать компонент PrivateRoute. Приведенный ниже код работает должным образом, поскольку компилятор правильно определяет тип (React.ReactNode).

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

Поэтому, чтобы решить проблему, я просто создаю новый тип только с параметрами, необходимыми для моего варианта использования.

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;