React router v4 - Authorized routes with HOC

11,810

Solution 1

Warning: The following answer uses React's old context API. If you are using V16.3+, the following answer does not apply to you

Ok, so, according to your logic, unauthorized users are prohibited to access the User component. Simple and fair. No problems with that.

But my concern is that are you checking if the user is logged in inside a component which unauthenticated users should not get into. This is incorrect in my opinion because:

  1. It's an extra journey for our program - adds an extra bit of unnecessary inefficiency. There is a possibility that from the router we go to User component and the latter sends us back to the former. Ping pong.

  2. The User component looks dirty. Having irrelevant logic. Yes irrelevant. Because authentication check should not be done in User component. User component should contain user related stuff.

What do you think if instead of getting inside the User component to check users authentication, we check this in the router? User component, as it name describe, is dedicated for users and the logic to check authentication should be taken out from there.

Ok, Thats cool. But how?

We can create a Higher-Order Component (HOC) which as an argument will take any component was pass to it. Then, we add authentication logic inside the HOC and finally, depending on the logic we use we can either redirect to the homepage or allow the request to the given component.

In order for the HOC to be able to do the above it needs access to:

  1. State. We need to know whether the user is logged in and the state is where we store such data.
  2. Router. We may need to redirect users.

Lets name the HOC required_auth. Here is the code of it:

import React, { Component } from 'react';
import { connect } from 'react-redux';

export default function(ComposedComponent) {
    class Authentication extends Component {
        static contextTypes = {
            router: React.PropTypes.object
        }

        componentWillMount() {
            if (!this.props.authenticated) {
                this.context.router.history.push('/');
            }
        }

        componentWillUpdate(nextProps) {
            if (!nextProps.authenticated) {
                this.context.router.history.push('/');
            }
        }

        render() {
            return <ComposedComponent {...this.props} />
        }
    }

    function mapStateToProps(state) {
        return { authenticated: state.auth.authed };
    }

    return connect(mapStateToProps)(Authentication);
}

As you can see, there is no black magic happening here. What might be confusing is

static contextTypes = {
    router: React.PropTypes.object
}

context is similar to props but it allows us to skip levels in our component hierarchy

Because this.context is very easy to access and abuse, React forces us to define the context in this way.

Do not use context unless you really know what you are doing. The use case for context is not that common. Read more on what the consequences could be here

To conclude on our HOC, it simply takes a component as an argument and it either redirect to the homepage or returns the component that we will pass to it.

Now to use it, in route file we import the HOC

 import RequiredAuth from './components/auth/required_auth';

and any routes which we want to protect from non-authoirzed users we simply route it like this:

<Route path="/user" component={RequiredAuth(User)}/>

The line above will either direct to homepage or returns the component which we are passing, User

References: https://facebook.github.io/react/docs/higher-order-components.html https://facebook.github.io/react/docs/context.html

Solution 2

Since you are using React Router 4, the answer by Matthew Barbara, while absolutely correct, is unnecessarily complicated. With RR4 you can simply use the Redirect component to handle your redirects. See https://reacttraining.com/react-router/web/api/Redirect for more info.

import React, { Component } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";

export default function requireAuth (WrappedComponent) {

  class Authentication extends Component {

    render () {

      if (!this.props.authenticated) {
        return <Redirect to="/"/>
      }

      return <WrappedComponent { ...this.props }/>
    }
  }

  function mapStateToProps (state) {
    return { authenticated: state.authenticated }
  }

  return connect(mapStateToProps)(Authentication);
}
Share:
11,810
Andrej Naumovski
Author by

Andrej Naumovski

Updated on September 05, 2022

Comments

  • Andrej Naumovski
    Andrej Naumovski over 1 year

    I have a problem to prevent unauthorized users from accessing authorized-only routes/components - such as logged in users dashboard

    I have the following code:

    import React from 'react'
    //other imports
    import {withRouter} from 'react-router'
    
    class User extends React.Component {
      constructor(props) {
        super(props)
        console.log('props', props)
        let user = JSON.parse(localStorage.getItem('userDetails'))
        if(!user || !user.user || props.match.params.steamId !== user.user.steamId) {
          props.history.push('/')
        } else {
          this.props.updateUserState(user)
          this.props.getUser(user.user.steamId)
        }
      }
    
      //render function
    }
    
    //mapStateToProps and mapDispatchToProps
    
    export default withRouter(connect(mapStateToProps, mapDispatchToProps)(User))
    

    The router:

    render() {
        return (
          <Router>
            <div>
              <Route exact path="/" component={Main}/>
              <Route path="/user/:steamId" component={User}/>
              <Route path="/completelogin" component={CompleteLogin}/>
            </div>
          </Router>
        )
      }
    

    I tried logging to check if the condition is entered and it is, however I get an error from the render function saying it cannot read properties of null.

    Is there a way to fix my problem and also a better approach to cater for my requirement? where specific components will be strictly accessible only to authorized users

    • Shubham Khatri
      Shubham Khatri over 6 years
      You should ideally have a authentication variable set to false and then check for the condition in componentWillMount or componentDidMount and not in constructor , see this stackoverflow.com/questions/44434041/…
    • Thomas Hennes
      Thomas Hennes over 6 years
      As @Shubham Khatri said, you should not put any logic inside the constructor, use lifecycle methods instead. Also, be careful, you're using both props and this.props inside the same method.
    • Andrej Naumovski
      Andrej Naumovski over 6 years
      @ShubhamKhatri Thank you, it worked when added to componentWillMount. I am still new to React and learning the lifecycle of a component.
    • Matthew Barbara
      Matthew Barbara over 6 years
      Can you show the code of the router? Where the actual routes are configured. Usually its in main/app/index file. I suspect the problem is coming from there.
    • Andrej Naumovski
      Andrej Naumovski over 6 years
      @MatthewBarbara I have added the code of the router to my question. Router is BrowserRouter. Also, adding my logic to componentWillMount worked when I render regular HTML in my render method, when I add a custom component to the User component it fails again.
  • Tomas
    Tomas about 6 years
    Am I missing something... how is this HOC supposed to have received props when none were sent through? How is it getting this.props.authenticated?
  • Matthew Barbara
    Matthew Barbara about 6 years
    mapStateToProps function is doing that part
  • Tinus Wagner
    Tinus Wagner almost 6 years
    I've come to the same solution you have, any tips for how we could test this HOC with Jest & Enzyme?