Как переписать защищенный / частный маршрут с помощью TypeScript и React-Router 4, 5 или 6?

Я пытался создать <PrivateRoute>, как описано в документах используя TypeScript. Может кто-нибудь мне помочь?

PrivateRoute в документе response-router:

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    fakeAuth.isAuthenticated ? (
      <Component {...props}/>
    ) : (
      <Redirect to={{pathname: '/login', state: { from: props.location }
   }}/>
  )
 )}/>
)

Ниже приведена моя версия TypeScript (не работает):

const PrivateRoute = (theProps: { path: string, component: React.SFC<RouteComponentProps<any> | undefined> | React.ComponentClass<RouteComponentProps<any> | undefined> }) => {
    return <Route path={theProps.path} render={props => (
        fakeAuth.isAuthenticated ? (
            <React.Component {...theProps} /> <!-- **** It will raise error *** -->
        ) : (
                <Redirect to={{
                    pathname: '/',
                    state: { from: props.location }
                }} />
            )
    )} />
}

<React.Component {...thisProps} /> не тот. Ошибка: NodeInvocationException: inst.render не является функцией TypeError: inst.render не является функцией


person Charlie    schedule 11.12.2017    source источник


Ответы (5)


Вероятно, ошибка связана с типизацией и неявным возвратом при рендеринге. Когда вы исправите это, вы получите что-то вроде этого:

const PrivateRoute = ({component, isAuthenticated, ...rest}: any) => {
    const routeComponent = (props: any) => (
        isAuthenticated
            ? React.createElement(component, props)
            : <Redirect to={{pathname: '/login'}}/>
    );
    return <Route {...rest} render={routeComponent}/>;
};

Этот компонент можно использовать так:

<PrivateRoute
    path='/private'
    isAuthenticated={this.props.state.session.isAuthenticated}
    component={PrivateContainer}
/>

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

Вероятно, расширение Route компонента - лучшая идея.

import * as React from 'react';
import {Redirect, Route, RouteProps} from 'react-router';

export interface ProtectedRouteProps extends RouteProps {
    isAuthenticated: boolean;
    authenticationPath: string;
}

export class ProtectedRoute extends Route<ProtectedRouteProps> {
    public render() {
        let redirectPath: string = '';
        if (!this.props.isAuthenticated) {
            redirectPath = this.props.authenticationPath;
        }

        if (redirectPath) {
            const renderComponent = () => (<Redirect to={{pathname: redirectPath}}/>);
            return <Route {...this.props} component={renderComponent} render={undefined}/>;
        } else {
            return <Route {...this.props}/>;
        }
    }
}

Таким образом, вы можете использовать компонент следующим образом:

const defaultProtectedRouteProps: ProtectedRouteProps = {
    isAuthenticated: this.props.state.session.isAuthenticated,
    authenticationPath: '/login',
};

<ProtectedRoute
    {...defaultProtectedRouteProps}
    exact={true}
    path='/'
    component={ProtectedContainer}
/>

Обновление (ноябрь 2019 г.)

Если вы предпочитаете писать функциональные компоненты, вы можете сделать это очень похожим образом. Это также работает с React Router 5:

import * as React from 'react';
import { Redirect, Route, RouteProps } from 'react-router';

export interface ProtectedRouteProps extends RouteProps {
  isAuthenticated: boolean;
  isAllowed: boolean;
  restrictedPath: string;
  authenticationPath: string;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = props => {
  let redirectPath = '';
  if (!props.isAuthenticated) {
    redirectPath = props.authenticationPath;
  }
  if (props.isAuthenticated && !props.isAllowed) {
    redirectPath = props.restrictedPath;
  }

  if (redirectPath) {
    const renderComponent = () => <Redirect to={{ pathname: redirectPath }} />;
    return <Route {...props} component={renderComponent} render={undefined} />;
  } else {
    return <Route {...props} />;
  }
};

export default ProtectedRoute;

Обновление (декабрь 2019 г.)

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

Перенаправление пользователя на страница, которую они запросили после успешной аутентификации с помощью response-router-dom

Обновление (март 2021 г.)

Приведенное выше решение немного устарело. Компонент ProtectedRoute можно просто записать следующим образом:

import { Redirect, Route, RouteProps } from 'react-router';

export type ProtectedRouteProps = {
  isAuthenticated: boolean;
  authenticationPath: string;
} & RouteProps;

export default function ProtectedRoute({isAuthenticated, authenticationPath, ...routeProps}: ProtectedRouteProps) {
  if(isAuthenticated) {
    return <Route {...routeProps} />;
  } else {
    return <Redirect to={{ pathname: authenticationPath }} />;
  }
};

Если вы используете React Router V6, вам необходимо заменить Redirect на Navigate. Полный пример с перенаправлением на изначально запрошенную страницу можно найти здесь:

person Robin    schedule 11.12.2017
comment
что, если я подключу ProtectedRoute к redux, чтобы получить свойство IsAuthenticated? это вызовет проблемы с производительностью? - person David Noreña; 12.01.2018
comment
Нет, не думаю. Есть только одно if / else больше по сравнению с первоначальным использованием Route. - person Robin; 13.01.2018
comment
Гениальное решение @Robin. :) Я добавлю сюда свои 2 цента: 1. ProtectedRouteProps не обязательно должен иметь isAuthenticated, потому что он сильно зависит от this.props.state. Это означает, что эта информация должна быть у каждого компонента. Вместо этого разработчики могут использовать какую-то наблюдаемую переменную на основе GlobalState / GlobalStore или Mobx для обнаружения isAuthenticated (или есть, реквизиты не будут переданы в - person Piyush; 29.05.2019
comment
@Piyush: Я не согласен с вашей идеей исключить isAuthenticated из реквизита, потому что компонент больше не будет использоваться повторно. Я предлагаю создать какой-то контейнерный компонент Router, где вы настраиваете все маршруты и связываете состояние. - person Robin; 09.11.2019
comment
Имеет смысл. Спасибо @Robin - person Piyush; 11.11.2019
comment
React настоятельно рекомендует сочинять, а не наследовать - person Joey Baruch; 19.10.2020
comment
@JoeyBaruch Я согласен. См. Update (Nov 2019) - person Robin; 19.10.2020
comment
Спасибо за ответ, он мне очень пригодился. Просто задайте короткий короткий вопрос, почему вы ставите render={undefined} на свой компонент перенаправления? Я немного запутался, спасибо! - person ysong4; 07.01.2021
comment
@ ysong4: Причина в том, что мы распространяем this.props, который может содержать render опору, и в этом случае мы хотим избавиться от этого. - person Robin; 07.01.2021

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

const PrivateRoute: React.SFC<RouteProps> = ({
  component: Component,
  ...rest
}: {
  component: React.ComponentType<RouteProps>;
}) => (
  <Route
    {...rest}
    render={props =>
      fakeAuth.isAuthenticated 
        ? <Component {...props} /> 
        : <Redirect to="/login" />
    }
  />
);
person Hunter McMillen    schedule 01.08.2018
comment
component должен быть типа React.ComponentType<RouteComponentProps<any>>, а не React.ComponentType<RouteProps>, не так ли? - person Nathan Hagen; 08.01.2019

Мой PrivateRoute

import React from 'react'
import {Redirect, Route, RouteProps} from 'react-router'

export interface IPrivateRouteProps extends RouteProps {
  isAuth: boolean // is authenticate route
  redirectPath: string // redirect path if don't authenticate route
}

const PrivateRoute: React.FC<IPrivateRouteProps> = (props) => {
   return props.isAuth ? (
    <Route {...props} component={props.component} render={undefined} />
  ) : (
    <Redirect to={{pathname: props.redirectPath}} />
  )
}

export default PrivateRoute

С использованием

<PrivateRoute isAuth={false} redirectPath="/login" path="/t1">
  <Pages.Profile /> your`s protected page
</PrivateRoute>
person George    schedule 25.05.2020

Это действительно помогло мне

import * as React from "react";
import { Route } from "react-router-dom";

interface IProps {
    exact?: boolean;
    path: string;
    component: React.ComponentType<any>;
}

const LoggedOutRoute = ({
    component: Component,
    ...otherProps
}: IProps) => (
    <>
        <header>Logged Out Header</header>
        <Route
            render={otherProps => (
                <>
                    <Component {...otherProps} />
                </>
            )}
        />
        <footer>Logged Out Footer</footer>
    </>
);

export default LoggedOutRoute;

Источник: https://medium.com/octopus-wealth/authenticated-routing-with-react-react-router-redux-typescript-677ed49d4bd6

person achin mandotia    schedule 21.08.2020

Мы можем написать, как показано ниже, без предоставления очень явных и точных типов или интерфейсов в tsx. Просто напишите как - {component: Component, ... rest}: any- as type, и все готово.

  export default function PrivateRoute({ component: Component, ...rest }: any) {
      const { currentUser } = useAuth();

      return (
        <Route
          {...rest}
          render={(props) => {
            return currentUser ? (
              <Component {...props} />
            ) : (
              <Redirect to="/login" />
            );
          }}
        ></Route>
      );
    }
person Ashutosh S    schedule 10.07.2021