15

I found this (reacttraining.com) site, which explains react-router with some examples. But I am not be able to do this with a typescript class. What I want to do is extend the Route class to build my own one. Right now I want to implement it in typescript for authentication as in the following example from the site.

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

I searched a lot, but couldn't find a site that explains the function to implement and which typed properties to call to nested routes. An ES6 class will be also helpful, thank you.

arpl
  • 3,505
  • 3
  • 18
  • 16
CordlessWool
  • 1,388
  • 4
  • 17
  • 35

7 Answers7

20

Here's my best shot so far, although there's still one any remaining :)

import * as React from "react"
import {Redirect, Route, RouteComponentProps, RouteProps} from "react-router-dom"

type RouteComponent = React.StatelessComponent<RouteComponentProps<{}>> | React.ComponentClass<any>

const AUTHENTICATED = false // TODO: implement authentication logic

export const PrivateRoute: React.StatelessComponent<RouteProps> = ({component, ...rest}) => {
  const renderFn = (Component?: RouteComponent) => (props: RouteProps) => {
    if (!Component) {
      return null
    }

    if (AUTHENTICATED) {
      return <Component {...props} />
    }

    const redirectProps = {
      to: {
        pathname: "/auth/sign-in",
        state: {from: props.location},
      },
    }

    return <Redirect {...redirectProps} />
  }

  return <Route {...rest} render={renderFn(component)} />
}
Jacka
  • 2,270
  • 4
  • 27
  • 34
  • 1
    Thx for sharing your Solution! What would be the best way to use the PrivateRoute with components that have their own Properties? – Oleg Hein Jan 09 '18 at 16:31
10

Regarding Redux ...

Jacka's answer helped me alot, but i had a difficult time connecting the PrivateRoute component to redux. Furthermore i wanted to abstract the resulting Route component to work e.g. as a LoggedInRoute, NotLoggedInRoute or in general a Route which presents it's component if a condition is fulfilled or redirects to a specified location otherwise:

Note: Written with redux 4, react-router-dom 4 and typescript 2.9.

import * as H from 'history';
import * as React from 'react';
import { connect, MapStateToPropsParam } from 'react-redux';
import { Redirect, Route, RouteComponentProps, RouteProps } from 'react-router';

export interface ConditionalRouteProps extends RouteProps {
  routeCondition: boolean;
  redirectTo: H.LocationDescriptor;
}

export class ConditionalRoute extends React.Component<ConditionalRouteProps> {
  public render() {
    // Extract RouteProps without component property to rest.
    const { component: Component, routeCondition, redirectTo, ...rest } = this.props;
    return <Route {...rest} render={this.renderFn} />
  }

  private renderFn = (renderProps: RouteComponentProps<any>) => {
    if (this.props.routeCondition) {
      const { component: Component } = this.props; // JSX accepts only upprcase.
      if (!Component) {
        return null;
      }
      return <Component {...renderProps} />
    }

    return <Redirect to={this.props.redirectTo} />;
  };
}

export function connectConditionalRoute<S>(mapStateToProps: MapStateToPropsParam<ConditionalRouteProps, RouteProps, S>) {
  return connect<ConditionalRouteProps, {}, RouteProps, S>(mapStateToProps)(ConditionalRoute);
}

You can either use the ConditionalRoute component without connecting it and use your component's local state, e.g.:

interface RootState {
  loggedIn: boolean;
}

export class Root extends React.Component<RootProps, RootState> {
  /* skipped initialState and setState(...) calls */

  public render() {
    return (
      <Switch>
        <ConditionalRoute
          path="/todos"
          component={TodoPage}
          routeCondition={this.state.loggedIn}
          redirectTo="/login" />
        <ConditionalRoute
          path="/login"
          component={LoginPage}
          routeCondition={!this.state.loggedIn}
          redirectTo="/" />
        <Redirect to="/todos" />
      </Switch>
    );
  }
}

Or use the utility function connectConditionalRoute<S>(...) to use your redux store:

const loginRoute = '/login';
const todosRoute = '/todos';

const LoggedInRoute = connectConditionalRoute<RootState>(state => ({
  redirectTo: loginRoute,
  routeCondition: state.isLoggedIn,
}));

const NotLoggedInRoute = connectConditionalRoute<RootState>(state => ({
  redirectTo: todosRoute,
  routeCondition: !state.isLoggedIn
}));

const Root: React.SFC = () => (
  <Switch>
    <LoggedInRoute path="/todos" component={TodoPage} />
    <NotLoggedInRoute path="/login" component={LoginPage} />
    <Redirect to="/todos" />
  </Switch>
);

The behaviour in the provided sample: Unauthorized users visit /todos, get redirected to /login, authorized users visit /login, get redirected to /todos. Whenever the redux store's isLoggedIn changes, the connected components are updated and redirect the user automatically.

sn42
  • 2,353
  • 1
  • 15
  • 27
6

here is my solution using "react-router-dom": "^4.4.0-beta.6" and "typescript": "3.2.2"

import React, { FunctionComponent } from "react";
import {
  Route, 
  Redirect,
  RouteProps, 
  RouteComponentProps
} from "react-router-dom";

interface PrivateRouteProps extends RouteProps {
  component:
    | React.ComponentType<RouteComponentProps<any>>
    | React.ComponentType<any>;
}

const PrivateRoute: FunctionComponent<PrivateRouteProps> = ({
  component: Component,
  ...rest
}) => {
  return (
    <Route
      {...rest}
      render={props =>
        true ? ( //put your authenticate logic here
          <Component {...props} />
        ) : (
          <Redirect
            to={{
              pathname: "/signin"
            }}
          />
        )
      }
    />
  );
};

export default PrivateRoute;
buncis
  • 2,148
  • 1
  • 23
  • 25
3

You could use any.

const PrivateRoute = ({component: Component, ...rest }: any) => (
  <Route {...rest} render={PrivateRender(Component)} />
);

const PrivateRender = (Component: any) => {
  return (props: any) => {
    return <Component {...props}/>;
  };
};
  • 2
    using any is just shutting the linter down, but not giving the static typing benefits like autocomplete/intellisense/error before app is run. – buncis Feb 08 '19 at 09:58
3

I was looking for same thing. The question is old but maybe someone is still looking for it. Here is what I come up with (All types properly used from react-router 4):

interface PrivateRouteProps extends RouteProps {
  component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;

export class PrivateRoute extends Route<PrivateRouteProps> {
  render () {
    const {component: Component, ...rest}: PrivateRouteProps = this.props;
    const renderComponent: RenderComponent = (props) => (
      AuthenticationService.isAuthenticated()
        ? <Component {...props} />
        : <Redirect to='/login' />
    );

    return (
      <Route {...rest} render={renderComponent} />
    );
  }
}
Doğancan Arabacı
  • 3,934
  • 2
  • 17
  • 25
1

Here is a really simple way to do this using function components since the new react router version allows you to access everything through hooks:

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

const PrivateRoute = (props: RouteProps) => {
  const { isAuthenticated } = useYourSessionProviderContext()
  if (isAuthenticated) {
    return <Route {...props} />
  } else {
    return <Redirect to='/login' />
  }
}

export default PrivateRoute


0

The solutions proposed here didn't work for me because I was using both component and render params in my original Routes. In this solution you can use any configuration of Route in your custom PrivateRoute instead of only the component param.

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

interface PrivateRouteProps extends RouteProps {
    isAuthenticated: boolean;
}

export class PrivateRoute extends Route<PrivateRouteProps> {
    render() {
        return (
            <Route render={(props: RouteComponentProps) => {
                if(!this.props.isAuthenticated()) {
                    return <Redirect to='/login' />
                } 

                if(this.props.component) {
                    return React.createElement(this.props.component);
                } 

                if(this.props.render) {
                    return this.props.render(props);
                }
            }} />
        );
    }
}

Examples:

<PrivateRoute 
    path={'/dashboard'} 
    component={DashboardPage} 
    isAuthenticated={props.isAuthenticated}
/>
<PrivateRoute 
    path={'/checkout'} 
    isAuthenticated={props.isAuthenticated}
    render={() => (
       <CheckoutPage auth={props.auth} />
    )} 
/>
Jonathan Eckman
  • 2,071
  • 3
  • 23
  • 49