163

I was trying to implement authenticated routes but found that React Router 4 now prevents this from working:

<Route exact path="/" component={Index} />
<Route path="/auth" component={UnauthenticatedWrapper}>
    <Route path="/auth/login" component={LoginBotBot} />
</Route>
<Route path="/domains" component={AuthenticatedWrapper}>
    <Route exact path="/domains" component={DomainsIndex} />
</Route>

The error is:

Warning: You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored

In that case, what's the correct way to implement this?

It appears in react-router (v4) docs, it suggests something like

<Router>
    <div>
    <AuthButton/>
    <ul>
        <li><Link to="/public">Public Page</Link></li>
        <li><Link to="/protected">Protected Page</Link></li>
    </ul>
    <Route path="/public" component={Public}/>
    <Route path="/login" component={Login}/>
    <PrivateRoute path="/protected" component={Protected}/>
    </div>
</Router>

But is it possible to achieve this while grouping a bunch of routes together?


After some research, I came up with this:

import React, {PropTypes} from "react"
import {Route} from "react-router-dom"

export default class AuthenticatedRoute extends React.Component {
  render() {
    if (!this.props.isLoggedIn) {
      this.props.redirectToLogin()
      return null
    }
    return <Route {...this.props} />
  }
}

AuthenticatedRoute.propTypes = {
  isLoggedIn: PropTypes.bool.isRequired,
  component: PropTypes.element,
  redirectToLogin: PropTypes.func.isRequired
}

Is it correct to dispatch an action in render()? It feels wrong. It doesn't really seem correct with componentDidMount or some other hook, either.

Null
  • 1,950
  • 9
  • 30
  • 33
Jiew Meng
  • 84,767
  • 185
  • 495
  • 805
  • best to do on componetWillMount if not using server side rendering. – mfahadi Apr 02 '17 at 14:11
  • @mfahadi, thank you for the input. I am not using SSR yet, but if I want to use in the future, do I keep it in render? Also if user is redirected in `componentWillMount`, will they ever get to see the rendered output even for a split second? – Jiew Meng Apr 03 '17 at 04:13
  • I'm really sorry for saying that `componentWillMount()` is not called on SSR, it is `componentDidMount()` that is not called. as `componentWillMount()` is called before `render()`, so the user will not see anything of new component. so it is best place to check for. – mfahadi Apr 03 '17 at 10:58
  • 1
    you could just use the `` [from the docs](https://reacttraining.com/react-router/web/api/Redirect) instead of calling the dispatch action – Fuzail l'Corder Aug 21 '17 at 08:31
  • Out of curiosity has anyone come up with a clean way to do the _reverse_ of what OP is asking? i.e. how to declare a route like `/login` that is _only accessible_ if the user is NOT logged in/authenticated? – mecampbellsoup Mar 17 '21 at 21:51

22 Answers22

328

You're going to want to use the Redirect component. There's a few different approaches to this problem. Here's one I like, have a PrivateRoute component that takes in an authed prop and then renders based on that props.

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

Now your Routes can look something like this

<Route path='/' exact component={Home} />
<Route path='/login' component={Login} />
<Route path='/register' component={Register} />
<PrivateRoute authed={this.state.authed} path='/dashboard' component={Dashboard} />

If you're still confused, I wrote this post that may help - Protected routes and authentication with React Router v4

Tyler McGinnis
  • 34,836
  • 16
  • 72
  • 77
  • 2
    Oh this is similar to my solution, but it uses ``. The problem is `` does not seem to work with redux in my case? I need to dispatch an action – Jiew Meng Apr 03 '17 at 04:38
  • 4
    I don't know why, but adding `state: {from: props.location}}}` caused a `maximum call stack exceeded`. I had to remove it. Could you explain why this option is useful @Tyler McGinnis? – martpie Sep 15 '17 at 09:23
  • @KeitIG That's strange. It's useful because it tells you where you came from. An example would be if you wanted the user to authenticate then once they authenticate, take them back to the page they were trying to access before you redirected them. – Tyler McGinnis Sep 16 '17 at 03:56
  • To provide additional reading/resources, the ``react-router-dom`` docs have a great example similar to this solution that I've been following: https://reacttraining.com/react-router/web/example/auth-workflow – Alex Johnson Sep 19 '17 at 19:02
  • 1
    i'm persisting the prop `authed` with `redux-persist` , which when `true` still causes a momentary redirect to `"/login"`. what essentially happens is that at reload or refresh at route `"/dashboard'` with `authed === true` `Dashboard` component renders then the route changes to `"/login"` causing `"Login"` component to render and then finally the route changes back to `/dashboard` route and the `"Dashboard"` component renders. What could be the cause for this? I checked `redux-devtools` the `authed` prop is `true` the entire time these route changes are occurring. @Tyler McGinnis. – jasan Sep 23 '17 at 13:49
  • This works for me when I try and access the page directly from the browser. But if I click a link in the existing app, the check doesn't execute - that's probably because the Route component has already rendered. Any work arounds for that? – alvincrespo Oct 12 '17 at 20:28
  • I didnt understand the {...rest} part here @TylerMcGinnis what is it? what does it do? – faraz Oct 24 '17 at 03:52
  • 8
    @faraz This explains the `({component: Component, ...rest})` syntax. I had the same question lol! https://stackoverflow.com/a/43484565/6502003 – protoEvangelion Nov 06 '17 at 04:10
  • `PrivateRoute.propTypes = { component: PropTypes.func.isRequired, authed: PropTypes.object.isRequired, }` – protoEvangelion Nov 06 '17 at 04:11
  • if we are using object of routes then how can we create a protected route. const routes = { path: '/', childRoutes: [ { path: '/login', component: Login}, { path: '/transaction', component: Transaction, }, { path: '*', component: PageNotFound } ] } – ngLover Mar 13 '18 at 12:38
  • @TylerMcGinnis, this sound good for one single route, but how would one wrap all private routes with this pattern? – BernardA Apr 12 '18 at 15:14
  • @TylerMcGinnis Your [blog post](https://tylermcginnis.com/react-router-protected-routes-authentication/) was a great elaboration! Thanks! – gatlanticus May 14 '18 at 08:12
  • @TylerMcGinnis is any way of doing it for ssr as well? – Ankit Balyan May 15 '18 at 13:26
  • But how do you implement this call a function to the server to validate the user is an authenticated user? – applecrusher May 17 '18 at 04:07
  • @TylerMcGinnis What's the `Home` component for? Can you not do the auth check at root and either redirect to login or dashboard? I'm just getting a blank page: https://codesandbox.io/s/qx4n397moq – Paul Redmond Jan 07 '19 at 16:51
  • 2
    @TylerMcGinnis What if we need to use the render function for passing props to the component? – C Bauer Mar 10 '19 at 05:21
  • To fix the maximum stack call, I put `path='/login' exact={true}` before `PrivateRoute` – Taku Jun 06 '19 at 10:26
  • Ooo very nice!! – Sebastian Patten Jun 14 '19 at 04:33
  • I am getting ×TypeError: Object doesn't support property or method 'assign' near for return in PrivateRoute – Jignesh Shah Nov 01 '19 at 19:40
  • @TylerMcGinnis I tried following your approach but its not working for me. I am using React coreUI template for my purpose and finding it hard to integrate your route approach wit it. Can you suggest some help? – program_bumble_bee Nov 02 '19 at 11:39
  • Following on @CBauer's comment, I'm finding that this approach borks when the route is defined with the `render` prop instead of the `component` prop, like so` } />` – Shawn de Wet May 27 '20 at 05:11
  • @ShawndeWet I will post the solution I landed upon in a moment – C Bauer May 27 '20 at 22:31
  • As a side note this code works because you are passing `authed` as a prop to `PrivateRoute` however if the api call for `auth` is done inside the `PrivateRoute` component it won't work reliably because `onMount` won't necessarily be called each time. You can get around that by adding `location` from `useLocation` to the dependency array and checking it's the same as the path for the protected route and then doing the API call. – Kex Jul 31 '20 at 04:07
  • For those of you guys using Framer-motion and AnimatePresence wrapped around your `Switch`, make sure to wrap the `Redirect` in a `motion.div` with an exit animation. Gave me headache for awhile – J.E.C. Mar 16 '21 at 13:11
  • What if `authed === true` is a promise/async function? – Displee Nov 11 '21 at 14:03
36

All answers are outdated

In 2022 the render prop of the Route component is for legacy use according to the react-router-dom documentation is not even working anymore in V5 and in V6 was removed.

This works instead:

const RequireAuth: FC<{ children: React.ReactElement }> = ({ children }) => {
   const userIsLogged = useLoginStatus(); // Your hook to get login status

   if (!userIsLogged) {
      return <LoginPage />;
   }
   return children;
};

Usage:

/* A route that doesn't require login */
<Route
   path="sign-up"
   element={
      <SignUpPage />
   }
/>

/* A route that requires login */
<Route
   path="dashboard"
   element={
      <RequireAuth>
         <DashboardPage />
      </RequireAuth>
   }
/>

EDIT: I updated the code example to v6 of React Router

fermmm
  • 1,078
  • 1
  • 9
  • 17
  • This is brilliant, saved so much time for me! – coderpc Sep 21 '21 at 16:40
  • `useLoginStatus()` instead of this we can check the login status through local storage as well right? – thatman303 Sep 24 '21 at 06:36
  • 3
    `useLoginStatus()` is there just as an example of a line that gets the current login status – fermmm Sep 26 '21 at 22:43
  • This answer is also out-dated now. because `Route` only accepts elements `attrbute` now. see https://stackoverflow.com/questions/69975792/error-home-is-not-a-route-component-all-component-children-of-routes-mus – Fatemeh Karimi Aug 30 '22 at 10:23
  • I edited the code example for React Router v6 – fermmm Oct 13 '22 at 07:16
  • how do u achive this using `createBrowserRouter` ? – Shamseer Ahammed Oct 25 '22 at 09:53
  • 4
    Wouldn't this result in /dashboard showing a LoginPage? What if I want to actually redirect to /Login? – chesscov77 Nov 21 '22 at 09:12
  • Exactly, this approach does not redirect. Redirecting is a more sophisticated approach since you need to save the original url the user was trying to reach before redirecting to the login page, because after login the user needs to be redirected back there – fermmm Nov 24 '22 at 23:05
  • Hi, It worked fine for me, the only problem is when the person do the login, after the jwt was expired and the link is still on /welcome, it return in the login page but the link still point in /welcome, so when in onClick function I do: navigate("/welcome") it stay in that page, any idea how to solve it? thanks – Robs Dec 06 '22 at 17:22
  • 1
    @fermmm great answers, but I agree with the above that i needs to redirect. You can do that like this: ```if (!auth.user) { return }``` – Flion Feb 06 '23 at 06:33
18

Tnx Tyler McGinnis for solution. I make my idea from Tyler McGinnis idea.

const DecisionRoute = ({ trueComponent, falseComponent, decisionFunc, ...rest }) => {
  return (
    <Route
      {...rest}

      render={
        decisionFunc()
          ? trueComponent
          : falseComponent
      }
    />
  )
}

You can implement that like this

<DecisionRoute path="/signin" exact={true}
            trueComponent={redirectStart}
            falseComponent={SignInPage}
            decisionFunc={isAuth}
          />

decisionFunc just a function that return true or false

const redirectStart = props => <Redirect to="/orders" />
MrDuDuDu
  • 534
  • 5
  • 12
12

(Using Redux for state management)

If user try to access any url, first i am going to check if access token available, if not redirect to login page, Once user logs in using login page, we do store that in localstorage as well as in our redux state. (localstorage or cookies..we keep this topic out of context for now).
since redux state as updated and privateroutes will be rerendered. now we do have access token so we gonna redirect to home page.

Store the decoded authorization payload data as well in redux state and pass it to react context. (We dont have to use context but to access authorization in any of our nested child components it makes easy to access from context instead connecting each and every child component to redux)..

All the routes that don't need special roles can be accessed directly after login.. If it need role like admin (we made a protected route which checks whether he had desired role if not redirects to unauthorized component)

similarly in any of your component if you have to disable button or something based on role.

simply you can do in this way

const authorization = useContext(AuthContext);
const [hasAdminRole] = checkAuth({authorization, roleType:"admin"});
const [hasLeadRole] = checkAuth({authorization, roleType:"lead"});
<Button disable={!hasAdminRole} />Admin can access</Button>
<Button disable={!hasLeadRole || !hasAdminRole} />admin or lead can access</Button>

So what if user try to insert dummy token in localstorage. As we do have access token, we will redirect to home component. My home component will make rest call to grab data, since jwt token was dummy, rest call will return unauthorized user. So i do call logout (which will clear localstorage and redirect to login page again). If home page has static data and not making any api calls(then you should have token-verify api call in the backend so that you can check if token is REAL before loading home page)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import history from './utils/history';


import Store from './statemanagement/store/configureStore';
import Privateroutes from './Privateroutes';
import Logout from './components/auth/Logout';

ReactDOM.render(
  <Store>
    <Router history={history}>
      <Switch>
        <Route path="/logout" exact component={Logout} />
        <Route path="/" exact component={Privateroutes} />
        <Route path="/:someParam" component={Privateroutes} />
      </Switch>
    </Router>
  </Store>,
  document.querySelector('#root')
);

History.js

import { createBrowserHistory as history } from 'history';

export default history({});

Privateroutes.js

import React, { Fragment, useContext } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { AuthContext, checkAuth } from './checkAuth';
import App from './components/App';
import Home from './components/home';
import Admin from './components/admin';
import Login from './components/auth/Login';
import Unauthorized from './components/Unauthorized ';
import Notfound from './components/404';

const ProtectedRoute = ({ component: Component, roleType, ...rest })=> { 
const authorization = useContext(AuthContext);
const [hasRequiredRole] = checkAuth({authorization, roleType});
return (
<Route
  {...rest}
  render={props => hasRequiredRole ? 
  <Component {...props} /> :
   <Unauthorized {...props} />  } 
/>)}; 

const Privateroutes = props => {
  const { accessToken, authorization } = props.authData;
  if (accessToken) {
    return (
      <Fragment>
       <AuthContext.Provider value={authorization}>
        <App>
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/login" render={() => <Redirect to="/" />} />
            <Route exact path="/home" component={Home} />
            <ProtectedRoute
            exact
            path="/admin"
            component={Admin}
            roleType="admin"
          />
            <Route path="/404" component={Notfound} />
            <Route path="*" render={() => <Redirect to="/404" />} />
          </Switch>
        </App>
        </AuthContext.Provider>
      </Fragment>
    );
  } else {
    return (
      <Fragment>
        <Route exact path="/login" component={Login} />
        <Route exact path="*" render={() => <Redirect to="/login" />} />
      </Fragment>
    );
  }
};

// my user reducer sample
// const accessToken = localStorage.getItem('token')
//   ? JSON.parse(localStorage.getItem('token')).accessToken
//   : false;

// const initialState = {
//   accessToken: accessToken ? accessToken : null,
//   authorization: accessToken
//     ? jwtDecode(JSON.parse(localStorage.getItem('token')).accessToken)
//         .authorization
//     : null
// };

// export default function(state = initialState, action) {
// switch (action.type) {
// case actionTypes.FETCH_LOGIN_SUCCESS:
//   let token = {
//                  accessToken: action.payload.token
//               };
//   localStorage.setItem('token', JSON.stringify(token))
//   return {
//     ...state,
//     accessToken: action.payload.token,
//     authorization: jwtDecode(action.payload.token).authorization
//   };
//    default:
//         return state;
//    }
//    }

const mapStateToProps = state => {
  const { authData } = state.user;
  return {
    authData: authData
  };
};

export default connect(mapStateToProps)(Privateroutes);

checkAuth.js

import React from 'react';

export const AuthContext = React.createContext();

export const checkAuth = ({ authorization, roleType }) => {
  let hasRequiredRole = false;

  if (authorization.roles ) {
    let roles = authorization.roles.map(item =>
      item.toLowerCase()
    );

    hasRequiredRole = roles.includes(roleType);
  }

  return [hasRequiredRole];
};

DECODED JWT TOKEN SAMPLE

{
  "authorization": {
    "roles": [
      "admin",
      "operator"
    ]
  },
  "exp": 1591733170,
  "user_id": 1,
  "orig_iat": 1591646770,
  "email": "hemanthvrm@stackoverflow",
  "username": "hemanthvrm"
}
Hemanthvrm
  • 2,319
  • 1
  • 17
  • 26
  • And how do you handle direct access to `Signin`? If a user knows he is not signed in, he should have an option to directly access Signin, right? – carkod Nov 08 '19 at 05:20
  • @carkod...By default if he try to access any route, he will be redirected to signin page...(since he wont be having token) – Hemanthvrm Nov 08 '19 at 17:53
  • @carkod.. once user clicked on logout or else my jwt refresh token expires ..i do call logout function where i clear localstorage and refresh window...hence localstorage wont be having token..it will automaticaly redirect to login page – Hemanthvrm Nov 08 '19 at 17:54
  • i do have a better version of it for those using redux..will update my answer in couple of days..thanks – – Hemanthvrm Nov 08 '19 at 17:54
5
const Root = ({ session }) => {
  const isLoggedIn = session && session.getCurrentUser
  return (
    <Router>
      {!isLoggedIn ? (
        <Switch>
          <Route path="/signin" component={<Signin />} />
          <Redirect to="/signin" />
        </Switch>
      ) : (
        <Switch>
          <Route path="/" exact component={Home} />
          <Route path="/about" component={About} />
          <Route path="/something-else" component={SomethingElse} />
          <Redirect to="/" />
        </Switch>
      )}
    </Router>
  )
}
Fellow Stranger
  • 32,129
  • 35
  • 168
  • 232
3

install react-router-dom

then create two components one for valid users and other for invalid users.

try this on app.js

import React from 'react';

import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect
} from 'react-router-dom';

import ValidUser from "./pages/validUser/validUser";
import InValidUser from "./pages/invalidUser/invalidUser";
const loggedin = false;

class App extends React.Component {
 render() {
    return ( 
      <Router>
      <div>
        <Route exact path="/" render={() =>(
          loggedin ? ( <Route  component={ValidUser} />)
          : (<Route component={InValidUser} />)
        )} />

        </div>
      </Router>
    )
  }
}
export default App;
Jose G Varanam
  • 767
  • 8
  • 19
3

Based on the answer of @Tyler McGinnis. I made a different approach using ES6 syntax and nested routes with wrapped components:

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

const PrivateRoute = ({ children, authed, ...rest }) =>
  <Route
    {...rest}
    render={(props) => authed ?
      <div>
        {Children.map(children, child => cloneElement(child, { ...child.props }))}
      </div>
      :
      <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
  />

export default PrivateRoute

And using it:

<BrowserRouter>
  <div>
    <PrivateRoute path='/home' authed={auth}>
      <Navigation>
        <Route component={Home} path="/home" />
      </Navigation>
    </PrivateRoute>

    <Route exact path='/' component={PublicHomePage} />
  </div>
</BrowserRouter>
Felipe Augusto
  • 7,733
  • 10
  • 39
  • 73
3

Heres how I solved it with React and Typescript. Hope it helps !

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

const PrivateRoute: FC<RouteProps> = ({ component: Component, ...rest }) => {
    if (!Component) {
      return null;
    }
    const isLoggedIn = true; // Add your provider here
    return (
      <Route
        {...rest}
            render={(props: RouteComponentProps<{}>) => isLoggedIn ? (<Component {...props} />) : (<Redirect to={{ pathname: '/', state: { from: props.location } }} />)}
      />
    );
  };

export default PrivateRoute;








<PrivateRoute component={SignIn} path="/signin" />
Mathyou
  • 651
  • 2
  • 10
  • 28
MaxThom
  • 1,165
  • 1
  • 13
  • 20
  • I'm getting `No render method found on the returned component instance: you may have forgotten to define render` error when I use this. My component is a functional component so obviously there is no render function. Does it need to be a functional component passed into it? – Mathyou Feb 07 '21 at 19:04
  • Never mind, it actually does work. The error was caused because I was using lowercase `component`, rather than `Component`. I'm a little bit confused as to how this part `component: Component` works. – Mathyou Feb 07 '21 at 19:21
  • Also, `React.SFC` is deprecated. Use `FC` instead. Imported as `import { FC } from "react";` – Mathyou Feb 07 '21 at 19:22
2

I know it's been a while but I've been working on an npm package for private and public routes.

Here's how to make a private route:

<PrivateRoute exact path="/private" authed={true} redirectTo="/login" component={Title} text="This is a private route"/>

And you can also make Public routes that only unauthed user can access

<PublicRoute exact path="/public" authed={false} redirectTo="/admin" component={Title} text="This route is for unauthed users"/>

I hope it helps!

  • can you please provide more examples including all the imports and wraps, for example in 2 publicroutes, 2 private routes and 2 PropsRoute, in the main App.js ? thank you – PolarBear10 Jul 31 '19 at 09:24
2

I implemented using-

<Route path='/dashboard' render={() => (
    this.state.user.isLoggedIn ? 
    (<Dashboard authenticate={this.authenticate} user={this.state.user} />) : 
    (<Redirect to="/login" />)
)} />

authenticate props will be passed to components e.g. signup using which user state can be changed. Complete AppRoutes-

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

import Home from '../pages/home';
import Login from '../pages/login';
import Signup from '../pages/signup';
import Dashboard from '../pages/dashboard';

import { config } from '../utils/Config';

export default class AppRoutes extends React.Component {

    constructor(props) {
        super(props);

        // initially assuming that user is logged out
        let user = {
            isLoggedIn: false
        }

        // if user is logged in, his details can be found from local storage
        try {
            let userJsonString = localStorage.getItem(config.localStorageKey);
            if (userJsonString) {
                user = JSON.parse(userJsonString);
            }
        } catch (exception) {
        }

        // updating the state
        this.state = {
            user: user
        };

        this.authenticate = this.authenticate.bind(this);
    }

    // this function is called on login/logout
    authenticate(user) {
        this.setState({
            user: user
        });

        // updating user's details
        localStorage.setItem(config.localStorageKey, JSON.stringify(user));
    }

    render() {
        return (
            <Switch>
                <Route exact path='/' component={Home} />
                <Route exact path='/login' render={() => <Login authenticate={this.authenticate} />} />
                <Route exact path='/signup' render={() => <Signup authenticate={this.authenticate} />} />
                <Route path='/dashboard' render={() => (
                    this.state.user.isLoggedIn ? 
                            (<Dashboard authenticate={this.authenticate} user={this.state.user} />) : 
                            (<Redirect to="/login" />)
                )} />
            </Switch>
        );
    }
} 

Check the complete project here: https://github.com/varunon9/hello-react

Varun Kumar
  • 2,543
  • 1
  • 23
  • 22
2

The accepted answer is good, but it does NOT solve the problem when we need our component to reflect changes in URL.

Say, your component's code is something like:

export const Customer = (props) => {

   const history = useHistory();
   ...

}

And you change URL:

const handleGoToPrev = () => {
    history.push(`/app/customer/${prevId}`);
}

The component will not reload!


A better solution:

import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import store from '../store/store';

export const PrivateRoute = ({ component: Component, ...rest }) => {

  let isLoggedIn = !!store.getState().data.user;

  return (
    <Route {...rest} render={props => isLoggedIn
      ? (
        <Component key={props.match.params.id || 'empty'} {...props} />
      ) : (
        <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
      )
    } />
  )
}

Usage:

<PrivateRoute exact path="/app/customer/:id" component={Customer} />
Alex Herman
  • 2,708
  • 4
  • 32
  • 53
2

I love @fermmm answer but in his implementation the rendered component will not match with the url if the user is not logged in. Thus it might be confusing for a visitor.

So, instead of

return (
  <Route {...props}>{userIsLogged ? props.children : <LoginPage/>}</Route>
);

I would suggest using:

return (
  <Route {...props}>
    {userIsLogged ? (
      props.children
     ) : (
       <Redirect
         to={{
            pathname: "/login",
            state: { from: location },
          }}
        />
    )}
  </Route>
);

In this case you will still get the component rendered but "/login" in the URL instead of the previous route segment.

JW Geertsma
  • 857
  • 3
  • 13
  • 19
1

It seems your hesitation is in creating your own component and then dispatching in the render method? Well you can avoid both by just using the render method of the <Route> component. No need to create a <AuthenticatedRoute> component unless you really want to. It can be as simple as below. Note the {...routeProps} spread making sure you continue to send the properties of the <Route> component down to the child component (<MyComponent> in this case).

<Route path='/someprivatepath' render={routeProps => {

   if (!this.props.isLoggedIn) {
      this.props.redirectToLogin()
      return null
    }
    return <MyComponent {...routeProps} anotherProp={somevalue} />

} />

See the React Router V4 render documentation

If you did want to create a dedicated component, then it looks like you are on the right track. Since React Router V4 is purely declarative routing (it says so right in the description) I do not think you will get away with putting your redirect code outside of the normal component lifecycle. Looking at the code for React Router itself, they perform the redirect in either componentWillMount or componentDidMount depending on whether or not it is server side rendering. Here is the code below, which is pretty simple and might help you feel more comfortable with where to put your redirect logic.

import React, { PropTypes } from 'react'

/**
 * The public API for updating the location programatically
 * with a component.
 */
class Redirect extends React.Component {
  static propTypes = {
    push: PropTypes.bool,
    from: PropTypes.string,
    to: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object
    ])
  }

  static defaultProps = {
    push: false
  }

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  }

  isStatic() {
    return this.context.router && this.context.router.staticContext
  }

  componentWillMount() {
    if (this.isStatic())
      this.perform()
  }

  componentDidMount() {
    if (!this.isStatic())
      this.perform()
  }

  perform() {
    const { history } = this.context.router
    const { push, to } = this.props

    if (push) {
      history.push(to)
    } else {
      history.replace(to)
    }
  }

  render() {
    return null
  }
}

export default Redirect
Todd Chaffee
  • 6,754
  • 32
  • 41
1

My Previous answer is not scalable. Here is what I think is good approach-

Your Routes-

<Switch>
  <Route
    exact path="/"
    component={matchStateToProps(InitialAppState, {
      routeOpen: true // no auth is needed to access this route
    })} />
  <Route
    exact path="/profile"
    component={matchStateToProps(Profile, {
      routeOpen: false // can set it false or just omit this key
    })} />
  <Route
    exact path="/login"
    component={matchStateToProps(Login, {
      routeOpen: true
    })} />
  <Route
    exact path="/forgot-password"
    component={matchStateToProps(ForgotPassword, {
      routeOpen: true
    })} />
  <Route
    exact path="/dashboard"
    component={matchStateToProps(DashBoard)} />
</Switch>

Idea is to use a wrapper in component props which would return original component if no auth is required or already authenticated otherwise would return default component e.g. Login.

const matchStateToProps = function(Component, defaultProps) {
  return (props) => {
    let authRequired = true;

    if (defaultProps && defaultProps.routeOpen) {
      authRequired = false;
    }

    if (authRequired) {
      // check if loginState key exists in localStorage (Your auth logic goes here)
      if (window.localStorage.getItem(STORAGE_KEYS.LOGIN_STATE)) {
        return <Component { ...defaultProps } />; // authenticated, good to go
      } else {
        return <InitialAppState { ...defaultProps } />; // not authenticated
      }
    }
    return <Component { ...defaultProps } />; // no auth is required
  };
};
Varun Kumar
  • 2,543
  • 1
  • 23
  • 22
  • if authentication is not required then dont pass component to the matchStateToProps function, with that you would eliminate the need for routeOpen flag – some_groceries Apr 10 '19 at 12:22
1

Here is the simple clean protected route

const ProtectedRoute 
  = ({ isAllowed, ...props }) => 
     isAllowed 
     ? <Route {...props}/> 
     : <Redirect to="/authentificate"/>;
const _App = ({ lastTab, isTokenVerified })=> 
    <Switch>
      <Route exact path="/authentificate" component={Login}/>
      <ProtectedRoute 
         isAllowed={isTokenVerified} 
         exact 
         path="/secrets" 
         component={Secrets}/>
      <ProtectedRoute 
         isAllowed={isTokenVerified} 
         exact 
         path="/polices" 
         component={Polices}/>
      <ProtectedRoute 
         isAllowed={isTokenVerified} 
         exact 
         path="/grants" component={Grants}/>
      <Redirect from="/" to={lastTab}/>
    </Switch>

isTokenVerified is a method call to check the authorization token basically it returns boolean.

Anupam Maurya
  • 1,927
  • 22
  • 26
  • This is the only solution here I found to work if you are passing a Component or Children to the route. – Shawn Mar 24 '20 at 04:11
  • Note: I just called my isTokenVerified() in my ProtectedRoute funciton and I didn't need to pass the isAllowed prop on all the routes. – Shawn Mar 24 '20 at 04:13
1

This is just a basic approach for beginners not for professional redux developers

import React, { useState, useEffect } from "react";
import {
  Route,
  BrowserRouter as Router,
  Switch,
  Redirect,
} from "react-router-dom";
import Home from "./components/Home";
import Dashboard from "./components/Dashboard";
import Login from "./components/Login";

function App() {
  const [isAuth, setAuth] = useState(false);

  const checkAuth = () => {
     // Your auth logic here
     setAuth(true);
  };

  useEffect(() => {
    checkAuth();
  });

  return (
    <Router>
      <Switch>
        <Route
          path="/user/dashboard"
          render={(props) =>
            isAuth ? <Dashboard {...props} /> : <Redirect to="/" />
          }
        />
        <Route path="/login" component={Login} />
        <Route path="/" component={Home} />
      </Switch>
    </Router>
  );
}
Hashan Shalitha
  • 835
  • 8
  • 15
0

Here is my own approach

const RedirectionUnit = () => {
  const [user] = useContext(AuthContext);
  const pathname = useLocation().pathname;
  let redirectTo;
  if (user === null) redirectTo = "login";
  else if (pathname === "/")
    if (user.type === "supervisor"      ) redirectTo = "all-parteners";
    else if (user.type === "manager"    ) redirectTo = "all-employees";
    else if (user.type === "employee"   ) redirectTo = "unfinished-tasks";
  if (redirectTo && '/' + redirectTo !== pathname)
    return <Redirect to={redirectTo} />;
  return null;
};

const NavigationRoutes = () => {
  return (
    <>
      <Route component={RedirectionUnit} />
      {/* prettier-ignore */}
      <Switch>
        <Route exact path="/login"            component={Login} />
        <Route exact path="/logout"           component={Logout} />
        <Route exact path="/new-parteners"    component={NewParteners} />
        <Route exact path="/all-parteners"    component={AllParteners} />
        <Route exact path="/new-employees"    component={NewEmployees} />
        <Route exact path="/all-employees"    component={AllEmployees} />
        <Route exact path="/unfinished-tasks" component={UnfinishedTasks} />
        <Route exact path="/finished-tasks"   component={FinishedTasks} />
        <Route exact path="/finished-tasks"   component={FinishedTasks} />
        <Route component={NotFound} />
      </Switch>
    </>
  );
};
Mohammed Samir
  • 376
  • 3
  • 9
0

I was looking for a solution where my main router file had everything it needed to authenticate the routes. No nested component needed or complicated if else's. Below is my approach

import React from "react";
import { Routes, Route } from "react-router-dom";
import { Navigate } from "react-router-dom";
// Other imports


export default function AppRoutes() {
  // This coming from react-redux
  // After a user is logged in this will set in the global state
  const { currentUser } = useCurrentUser();

  const landing = <Landing />

  const authenticate = (component) => {
    return currentUser ? component : <Navigate to="/" />;
  }

  return (
    <Routes>
      <Route path="/" element={currentUser ? <Home /> : landing} />

      <Route path="/blogs/:id" element={authenticate(<Blog />)} />
      <Route path="/blogs/:id/edit" element={authenticate(<BlogEdit />)} />
      <Route path="/profile" element={authenticate(<Profile />)} />
      <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  );
}
Abhishek Sarkar
  • 488
  • 1
  • 4
  • 12
0

Based on the solution of @MaxThom for TypeScript, here is an option to be able to pass a component or a render function to PrivateRoute:

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


const PrivateRoute: React.FC<RouteProps> = ({component, render, ...rest}) => {

  const userIsLogged = window.localStorage.getItem('currentUsecase');
  
   if (userIsLogged === undefined) return (
      <Route render={
         (props: RouteComponentProps<{}>) => <Redirect 
            to={{ pathname: '/', state: { from: props.location } }} 
         />
      }/>
   )
   
   return (
      <Route {...rest} render={render} component={component} />
   )
};

export default PrivateRoute;

I hope this helps.

0

For newer version of React !

There's a many different approaches to this problem. that work with older version but doesn't work for newer version of React.

React: ^ 18.2

React Router Dom: ^ 6.11

//App.js
<Route path="/"
       element={<HomePage /> } />

<Route path="/dashboard"
       element={<AuthMiddleware> <DashboardPage /> </AuthMiddleware>}
/>
// AuthMiddelware.js
import { Navigate } from "react-router-dom"

export const AuthMiddleware = (props) => { 
    
    const token = true; // get login status here
    
    const { auth=token, children, redirect = '/login' } = props; 

    if (!auth) {
       return <Navigate to={redirect} replace />;
    }
    return children;
 };
GMKHussain
  • 3,342
  • 1
  • 21
  • 19
0

There is simple solution using loaders from React Router v6.

Example:

Suppose this is your router with one public route and two private routes:

import {createBrowserRouter} from "react-router-dom";

function AppRouter() {
    return createBrowserRouter(
        [
            // Public routes
            {path: "/login", element: <Login/>},
            // Private routes
            {path: "/accounts", element: <AccountsList/>, loader: PrivateLoader},
            {path: "/accounts/:accountId", element: <EditAccount/>, loader: PrivateLoader},
        ]
    );
}

Note that the two private routes have the key loader.

Here is the definition of PrivateLoader:

import {redirect} from "react-router-dom";
import * as Storage from "../infrastructure/Storage";

function PrivateLoader() {
    return Storage.isValidToken() ? null : redirect("/login");
}

The PrivateLoader function redirects the user to the login in case it is not authenticated or the token is expired.

For this particular example Storage is a component that gets a JWT from the local storage and checks if the token is still valid. We can replace Storage to check if the user is authenticated in other way we might need.

Source: Auth Example (using RouterProvider)

Julian Espinel
  • 2,586
  • 5
  • 26
  • 20
-1

I was also looking for some answer. Here all answers are quite good, but none of them give answers how we can use it if user starts application after opening it back. (I meant to say using cookie together).

No need to create even different privateRoute Component. Below is my code

    import React, { Component }  from 'react';
    import { Route, Switch, BrowserRouter, Redirect } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import store from './stores';
    import requireAuth from './components/authentication/authComponent'
    import SearchComponent from './components/search/searchComponent'
    import LoginComponent from './components/login/loginComponent'
    import ExampleContainer from './containers/ExampleContainer'
    class App extends Component {
    state = {
     auth: true
    }


   componentDidMount() {
     if ( ! Cookies.get('auth')) {
       this.setState({auth:false });
     }
    }
    render() {
     return (
      <Provider store={store}>
       <BrowserRouter>
        <Switch>
         <Route exact path="/searchComponent" component={requireAuth(SearchComponent)} />
         <Route exact path="/login" component={LoginComponent} />
         <Route exact path="/" component={requireAuth(ExampleContainer)} />
         {!this.state.auth &&  <Redirect push to="/login"/> }
        </Switch>
       </BrowserRouter>
      </Provider>);
      }
     }
    }
    export default App;

And here is authComponent

import React  from 'react';
import { withRouter } from 'react-router';
import * as Cookie from "js-cookie";
export default function requireAuth(Component) {
class AuthenticatedComponent extends React.Component {
 constructor(props) {
  super(props);
  this.state = {
   auth: Cookie.get('auth')
  }
 }
 componentDidMount() {
  this.checkAuth();
 }
 checkAuth() {
  const location = this.props.location;
  const redirect = location.pathname + location.search;
  if ( ! Cookie.get('auth')) {
   this.props.history.push(`/login?redirect=${redirect}`);
  }
 }
render() {
  return Cookie.get('auth')
   ? <Component { ...this.props } />
   : null;
  }
 }
 return  withRouter(AuthenticatedComponent)
}

Below I have written blog, you can get more depth explanation there as well.

Create Protected routes in ReactJS

nirmal
  • 2,143
  • 1
  • 19
  • 29