9
votes

Here is an example from react-router for how to add a component for protected routes:

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

https://reacttraining.com/react-router/web/example/auth-workflow

I have tried to implement this functionality in my Typescript project, using the example above as inspiration.

components/Routes

import PrivateRoute from '../../connectors/PrivateRoute';
<PrivateRoute path="/codes" component={SomePage} />

connectors/PrivateRoute

import { connect } from 'react-redux';
import { AppState } from 'app-types';
import PrivateRouteComponent from '../../components/PrivateRoute';

const mapStateToProps = (state: AppState) => {
    const isSignedIn = state.user.isSignedIn;

    return {
        isSignedIn
    };
};

const PrivateRoute = connect(
    mapStateToProps,
    null
)(PrivateRouteComponent);

export default PrivateRoute;

components/PrivateRoute

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

interface PrivateRouteProps {
    // tslint:disable-next-line:no-any
    component: any;
    isSignedIn: boolean;
    // tslint:disable-next-line:no-any
    location: any;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const { component: Component, isSignedIn, location, ...rest } = props;

    return (
        <Route
            {...rest}
            render={(routeProps) =>
                isSignedIn ? (
                    <Component {...routeProps} />
                ) : (
                        <Redirect
                            to={{
                                pathname: '/signin',
                                state: { from: location }
                            }}
                        />
                    )
            }
        />
    );
};

export default PrivateRoute;

Error

(105,18): Type '{ path: string; component: ConnectedComponentClass<typeof SomePage, Pick<SomePageProps, never>>; }' is not assignable to type 'Readonly<Pick<PrivateRouteProps, "location" | "component">>'.
  Property 'location' is missing in type '{ path: string; component: ConnectedComponentClass<typeof SomePage, Pick<SomePageProps, never>>; }'.
4

4 Answers

24
votes

The error occurs because PrivateRouteProps has a required property location that isn't provided when you use PrivateRoute in components/Routes.tsx. I assume that this location should instead come from the routeProps that the router automatically passes to the render function of the route, as it did in the original example. Once this is fixed, another error is exposed: components/Routes.tsx is passing a paths property that isn't declared in PrivateRouteProps. Since PrivateRoute is passing any prop it doesn't know about to Route, PrivateRouteProps should extend RouteProps from react-router so that PrivateRoute accepts all props accepted by Route.

Here is components/PrivateRoute.tsx after both fixes:

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

interface PrivateRouteProps extends RouteProps {
    // tslint:disable-next-line:no-any
    component: any;
    isSignedIn: boolean;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const { component: Component, isSignedIn, ...rest } = props;

    return (
        <Route
            {...rest}
            render={(routeProps) =>
                isSignedIn ? (
                    <Component {...routeProps} />
                ) : (
                        <Redirect
                            to={{
                                pathname: '/signin',
                                state: { from: routeProps.location }
                            }}
                        />
                    )
            }
        />
    );
};

export default PrivateRoute;
1
votes

I found Matt's answer very useful but needed it to work for children as well as component, so tweaked as follows:

import * as React from 'react';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { fakeAuth } from '../api/Auth';

interface PrivateRouteProps extends RouteProps {
  // tslint:disable-next-line:no-any
  component?: any;
  // tslint:disable-next-line:no-any
  children?: any;
}

const PrivateRoute = (props: PrivateRouteProps) => {
  const { component: Component, children, ...rest } = props;

  return (
    <Route
      {...rest}
      render={routeProps =>
        fakeAuth.isAuthenticated ? (
          Component ? (
            <Component {...routeProps} />
          ) : (
            children
          )
        ) : (
          <Redirect
            to={{
              pathname: '/signin',
              state: { from: routeProps.location },
            }}
          />
        )
      }
    />
  );
};

export default PrivateRoute;

Note: This happens to be using fakeAuth like the original training article rather than user1283776's isSignedIn redux stuff, but you get the idea.

1
votes

The current answers work but I would like to post my solution as I think it has a few advantages:

  • Not overriding the property component with the type any.
  • Leveraging the render method from the library to support both - <Route> Component and children props - without reimplementing already present framework logic/code.
  • Using Recipe: Static Typing from the official react-redux documentation

Example:

import * as React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import {
    Redirect,
    Route,
    RouteProps,
} from 'react-router-dom';
import { AppState } from '../store';

const mapState = (state: AppState) => ({
  loggedIn: state.system.loggedIn,
});

const connector = connect(
  mapState,
  { }
);

type PropsFromRedux = ConnectedProps<typeof connector>;

type Props = PropsFromRedux & RouteProps & {

};

const PrivateRoute: React.FC<Props> = props => {
    const { loggedIn, ...rest } = props;

    return ( !loggedIn ? <Redirect to="/login/" /> :
      <Route {...rest} />
    );
};

export default connector(PrivateRoute);
-1
votes

I use the type "React.ReactNode" for my children components instead of any.