63

I was trying to create a <PrivateRoute> as describe in the react-router documents using TypeScript. Can anyone help me out?

The privateRoute in react-router document:

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

Below is my TypeScript version(it won't work) :

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 }
                }} />
            )
    )} />
}

The <React.Component {...thisProps} /> is not right. The error is: NodeInvocationException: inst.render is not a function TypeError: inst.render is not a function

Robin
  • 8,162
  • 7
  • 56
  • 101
Charlie
  • 2,141
  • 3
  • 19
  • 35

11 Answers11

147

Original Answer (2017)

(Updated answers below)

Probably the error has to do with the typing and the implicit return in rendering. When you fix this you get ultimately to something like this:

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

This component can be used like this:

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

There are a few draw backs with the solution above. One of the is that you lose type safety.

Probably extending the Route component is the better idea.

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}/>;
        }
    }
}

So you can use the component like this:

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

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

Update (Nov 2019)

If you prefer to write functional components you can do it in a very similar manner. This also works with 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;

Update (Dec 2019)

If you want to redirect a user to the path the user wanted to access first, you need to remember the path, so you can redirect after successful authentication. The following answer will guide you through that:

Redirecting a user to the page they requested after successful authentication with react-router-dom

Update (Mar 2021)

The solution above is a bit outdated. The ProtectedRoute component can simply be written as follows:

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 }} />;
  }
};

If you use React Router V6 you need to replace Redirect with Navigate. A full example with redirection to the originally requested page can be found here:

Update (Jan 2022)

As children of <Routes> need to be <Route> elements the <ProtectedRoute> can be changed to:

export type ProtectedRouteProps = {
  isAuthenticated: boolean;
  authenticationPath: string;
  outlet: JSX.Element;
};

export default function ProtectedRoute({isAuthenticated, authenticationPath, outlet}: ProtectedRouteProps) {
  if(isAuthenticated) {
    return outlet;
  } else {
    return <Navigate to={{ pathname: authenticationPath }} />;
  }
};

<ProtectedRoute> can now be applied like follows:

const defaultProtectedRouteProps: Omit<ProtectedRouteProps, 'outlet'> = {
  isAuthenticated: !!sessionContext.isAuthenticated,
  authenticationPath: '/login',
};

return (
  <div>
    <Routes>
      <Route path='/' element={<Homepage />} />
      <Route path='dashboard' element={<ProtectedRoute {...defaultProtectedRouteProps} outlet={<Dashboard />} />} />
      <Route path='protected' element={<ProtectedRoute {...defaultProtectedRouteProps} outlet={<Protected />} />} />
      <Route path='nested' element={<ProtectedRoute {...defaultProtectedRouteProps} outlet={<Layout />} />}>
        <Route path='one' element={<Protected />} />
        <Route path='two' element={<Protected />} />
      </Route>
      <Route path='login' element={<Login />} />
    </Routes>
  </div>
);

I've also updated the React Router 6 example. By now there is even an official guide about this: https://reactrouter.com/docs/en/v6/examples/auth

sallf
  • 2,583
  • 3
  • 19
  • 24
Robin
  • 8,162
  • 7
  • 56
  • 101
  • what if I connect ProtectedRoute to redux to get the IsAuthenticated property ? would it cause performance issues ? – David Noreña Jan 12 '18 at 20:18
  • 1
    No, I don't think so. There is just one if/else more compared to the original use of `Route`. – Robin Jan 12 '18 at 22:29
  • 1
    Brilliant solution @Robin. :) I'll add my 2 cents here: 1. ProtectedRouteProps need not have isAuthenticated because it highly depends on this.props.state. Which means each component will have to have that info. Instead Devs can use some sort of GlobalState / GlobalStore or Mobx based observable variable to detect isAuthenticated(or is, props will not get passed to – Piyush May 29 '19 at 05:29
  • 1
    @Piyush: I disagree with your idea to omit the `isAuthenticated` from the props, because the component won't be reusable anymore. I suggest to create some kind of Router container component, where you set up all the routes and bind the state. – Robin Nov 09 '19 at 10:12
  • Makes sense. Thanks @Robin – Piyush Nov 10 '19 at 23:24
  • react strongly recommends to compose rather than inherit – Joey Baruch Oct 19 '20 at 18:48
  • @JoeyBaruch I agree. See `Update (Nov 2019)` – Robin Oct 19 '20 at 18:50
  • Thanks for your answer, I found it very useful. Just have a quick short question, why do you put a `render={undefined}` on your redirect component? I am a little confused, thanks! – ysong4 Jan 07 '21 at 02:59
  • @ysong4: The reason is, that we are spreading `this.props` which can contain a `render` prop and we want to get rid of this in that case. – Robin Jan 07 '21 at 06:19
  • @Robin, sadly this solution is broken in the latest v6, perhaps a bug? – sjbuysse Sep 23 '21 at 09:01
  • 2
    [ProtectedRoute] is not a component. All component children of must be a or :( – kevinob Dec 30 '21 at 13:50
  • 1
    @kevinob You are right. I've updated the solution. – Robin Jan 30 '22 at 22:00
11

You can still use the SFC form, which I find a little cleaner. Just mix in any props you need with the RouteProps:

const PrivateRoute: React.SFC<RouteProps> = ({
  component: Component,
  ...rest
}: {
  component: React.ComponentType<RouteProps>;
}) => (
  <Route
    {...rest}
    render={props =>
      fakeAuth.isAuthenticated 
        ? <Component {...props} /> 
        : <Redirect to="/login" />
    }
  />
);
Hunter McMillen
  • 59,865
  • 24
  • 119
  • 170
10

For react-router-dom (v6.0.2) , you can use the following code for your PrivateRoute component:

import { FC } from 'react';
import { useAppSelector } from 'app/hooks';
import { Navigate } from 'react-router-dom';

interface PropType {
    component: React.FC;
}

const PrivateRoute: FC<PropType> = ({ component: Component }) => {
    const { isAuthenticated } = useAppSelector(state => state.auth);

    if (isAuthenticated) return <Component />;
    return <Navigate to='/login' />;
};

export default PrivateRoute;

To use inside your App.tsx, you can use it as follows:

        <Routes>
            <Route path='/' element={<LandingPage />} />
            <Route path='/login' element={<LoginPage />} />
            <Route path='/home' element={<PrivateRoute component={HomePage} />} />
            <Route path='*' element={<NotFound />} />
        </Routes>
Shahmir Jadoon
  • 450
  • 4
  • 6
7

My 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

Using

<PrivateRoute isAuth={false} redirectPath="/login" path="/t1">
  <Pages.Profile /> your`s protected page
</PrivateRoute>
George
  • 71
  • 1
  • 1
5

This really helped me

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;

Source: https://medium.com/octopus-wealth/authenticated-routing-with-react-react-router-redux-typescript-677ed49d4bd6

41 72 6c
  • 1,600
  • 5
  • 19
  • 30
4

We can write as below without providing very explicit and exact types or interfaces in tsx. Just write like -{ component: Component, ...rest }: any- as type and we are done.

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

      return (
        <Route
          {...rest}
          render={(props) => {
            return currentUser ? (
              <Component {...props} />
            ) : (
              <Redirect to="/login" />
            );
          }}
        ></Route>
      );
    }
Ashutosh Shukla
  • 557
  • 5
  • 9
2

Using v6 of React-router-dom we handle the protected route in this format

Setting up the Auth protection component

import React from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { useAppSelector } from "../../state/hooks";

const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
  const {user} = <Your-State-Provider>// Redux/Context or even in-memory user
  const location = useLocation();
  return !user.isAuthenticated ? (
    <Navigate to={"/login"} state={{ from: location }} replace />
  ) : (
    children
  );
};

export default ProtectedRoute;

In this Basically The user authentication state will be checked then against that condition we user the <Navigate/> to redirect back to login page. We get the current location and pass it to the Navigate so that we redirect the user to the intended page after login automatically. We restructure the children props and render the children if the user is authenticated. The advantage of this is that we'll just wrap the element we want to render with the <ProtectedRoute>{children}</ProtectedRoute>.

Consuming the Protected Route

import { Fragment } from "react";
import ProtectedRoute from "./components/ProtectedRoute/ProtectedRoute";//Your protected route
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Login from "./pages/Login/Login";
import MainPage from "./pages/MainPage/MainPage";


const App = () => {
  return (
    <Router>
      <Fragment>
        <nav>
          <Link to="/admin" />
        </nav>
        <Routes>
          <Route
            path="/"
            element={
              <ProtectedRoute>
                <MainPage />
              </ProtectedRoute>
            }
          />
          <Route path="/login" element={<Login />} />
        </Routes>
      </Fragment>
    </Router>
  );
};

export default App;

Because react-router-dom v6 allows nesting of components in the route now we just wrap the component we want to protect with the ProtectedRoute eg

 <Route path="/" element={ <ProtectedRoute><Your-Protected-page /></ProtectedRoute>}/>
Felix Orinda
  • 593
  • 4
  • 20
1

Seems since react-router-dom 6.0.0-beta.4 for me only that worked:

App.tsx

import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom';

interface Props {}
export const App: React.FC<Props> = ({}) => {
    const isAuthenticated = true;
    return (
        <Router>
            <Routes>
                <Route path={`/`} element={isAuthenticated ? <AuthenticatedPage /> : <Navigate to={`/auth`} />} />
                <Route path={`/auth`} element={<AuthenticationPage />} />
            </Routes>
        </Router>
    );
};

https://github.com/remix-run/react-router/issues/8033

antokhio
  • 1,497
  • 2
  • 11
  • 16
1

Just to add what worked for me:

interface PrivateRouteProps extends RouteProps {
  component: React.FC<RouteProps>;
  path: string;
}

export default function PrivateRoute({
  component: Component,
  path,
}: PrivateRouteProps) {
  return (
    <Route
      path={path}
      render={(props) =>
        localStorage.getItem('user') ? (
          <Component {...props} />
        ) : (
          <Redirect
            to={{ pathname: '/login', state: { from: props.location } }}
          />
        )
      }
    />
  );
}

and can be used like this:

<PrivateRoute path="/user/dashboard" component={Dashboard} />
vikrant
  • 2,169
  • 1
  • 21
  • 27
0

Quick code snippet:

PrivateRote.tsx

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

interface PrivateRouteProps extends RouteProps {
  component: any
}

export const PrivateRoute = (props: PrivateRouteProps) => {
  const { component: Component, ...rest } = props
  const { isLogin} = useLogin() //true/false or something else

  return account ? <Route {...rest} render={props => <Component {...props} />} /> : <Redirect to="/" />
}

usage in App.tsx

<Router>
   <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/faq" component={Faq} />
      <PrivateRoute exact path="/profile" component={Profile} />
    </Switch>
</Router>
Erik P_yan
  • 608
  • 5
  • 6
-1

This is clean and simple.

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

import { RoutePaths } from "./RoutePaths";

interface Props extends RouteProps {
    isLoggedIn: boolean;
}

const AuthRoute: React.FC<Props> = ({ component: Component, ...rest }) => {
    if (!Component) {
        return null;
    }

    const { isLoggedIn } = rest;

    return (
        <Route
            {...rest}
            render={(props) =>
                isLoggedIn ? (
                    <Component {...props} />
                ) : (
                    <Redirect
                        to={{
                            pathname: RoutePaths.Auth,
                            /**
                             * For redirecting after login.
                             */
                            state: { from: props.location },
                        }}
                    />
                )
            }
        />
    );
};

export default AuthRoute;


Adiat Hasan
  • 372
  • 3
  • 12