6
votes

I'm trying to work with a team building a React application, and trying to figure out the best way to create a "higher-order" React component (one that wraps another) to perform Authentication in conjunction with the Redux data store.

My approach thus far has been to create a module that consists of a function that returns a new React component depending on whether or not there is an authenticated user.

export default function auth(Component) {

    class Authenticated extends React.Component {

        // conditional logic

        render(){
            const isAuth = this.props.isAuthenticated;

            return (
                <div>
                    {isAuth ? <Component {...this.props} /> : null}
                </div>
            )
        }
    }

    ...

    return connect(mapStateToProps)(Authenticated);

}

This makes it easy for other people on my team to specify whether or not a component requires certain permissions.

render() {
    return auth(<MyComponent />);
}

If you are performing role-based checks, this approach makes sense, as you may only have a few roles. In such a case, you could just call auth(<MyComponent />, admin).

Passing arguments becomes unwieldy for permissions-based checks. It may however be feasible to specify permissions at the component level as the components are being constructed (as well as manageable in a team environment). Setting static methods/properties seems like a decent solution, but, as far as I can tell, es6 classes export as functions, which don't reveal callable methods.

Is there a way to access the properties/methods of an exported React component such that they can be accessed from a containing component?

5

5 Answers

2
votes

onEnter is great, and useful in certain situations. However, here are some common authentication and authorization problems onEnter does not solve:

  • Decide authentication/authorization from redux store data (there are some workarounds)
  • Recheck authentication/authorization if the store updates (but not the current route)

  • Recheck authentication/authorization if a child route changes underneath the protected route

An alternative approach is to use Higher Order Components.

You can use Redux-auth-wrapper provides higher-order components for easy to read and apply authentication and authorization constraints for your components.


  • To get child methods you can use:refs, callback and callback from refs

  • To get child props you can use:this.refs.child.props.some or compInstance.props.some

Example for methods and props:

class Parent extends Component {
    constructor(props){
        super(props);
        this.checkChildMethod=this.checkChildMethod.bind(this);
        this.checkChildMethod2=this.checkChildMethod2.bind(this);
        this.checkChildMethod3=this.checkChildMethod3.bind(this);
    }
    checkChildMethod(){
        this.refs.child.someMethod();
        console.log(this.refs.child.props.test);
    }
    checkChildMethod2(){
        this._child2.someMethod();
        console.log(this._child2.props.test);
    }
    checkChildMethod3(){
        this._child3.someMethod();
        console.log(this._child3.props.test);
    }
    render(){
        return (
            <div>
                Parent
                <Child ref="child" test={"prop of child"}/>
                <ChildTwo ref={c=>this._child2=c} test={"prop of child2"}/>
                <ChildThree returnComp={c=>this._child3=c} test={"prop of child3"}/>
                <input type="button" value="Check method of child" onClick={this.checkChildMethod}/>
                <input type="button" value="Check method of childTwo" onClick={this.checkChildMethod2}/>
                <input type="button" value="Check method of childThree" onClick={this.checkChildMethod3}/>
            </div>
        );
    }
}

class Child extends Component {
    someMethod(){
        console.log('someMethod Child');
    }
    render(){
        return (<div>Child</div>);
    }
}
class ChildTwo extends Component {
    someMethod(){
        console.log('someMethod from ChildTwo');
    }
    render(){
        return (<div>Child</div>);
    }
}
class ChildThree extends Component {
    componentDidMount(){
        this.props.returnComp(this);
    }
    someMethod(){
        console.log('someMethod from ChildThree');
    }
    render(){
        return (<div>Child</div>);
    }
}
3
votes

This seems a very interesting possibility. I hit this question Googling the same issue, and this is a new library so I figured doesn't hurt to link it in case anyone else can be helped by it. I have not decided if I am going to go this route myself yet, as I am 15 minutes into the Google-palooza. It's called CASL.

Link to the Article Explaining the Library

Link to the Library

Example Code from the Library per request:

if (ability.can('delete', post)) {
  <button onClick={this.deletePost.bind(this}>Delete</button>
}

replaces something like:

if (user.role === ADMIN || user.auth && post.author === user.id) {
  <button onClick={this.deletePost.bind(this}>Delete</button>
}

Which in the article the author furthered with a custom component to get:

<Can run="delete" on={this.props.todo}>
  <button onClick={this.deleteTodo.bind(this}>Delete</button>
</Can>

It basically allows the developer to be more declarative in their code for ease of use and maintainability.

1
votes

If you use react-router, the recommended way to handle authorization is through the onEnter property in the Route component.

<Route path="/" component={Component} onEnter={Component.onEnter} />  

See the docs.

And it's also an answer to your question:

Is there a way to access the properties/methods of an exported React component such that they can be accessed from a containing component?

So just make them static properties/methods (like Component.onEnter).

1
votes

I found an article here, the gist of which i am writing here. You can add a prop to you component like so

<Route path="/" component={App}>

//BOD routes
<Route authorisedUsers={['KR']} path="/home" component={HomeContainer} />

//HR routes
<Route authorisedUsers={['HR']} path="/hrhome" component={HRDashboardContainer} />

//common routes    
<Route authorisedUsers={['KR', 'HR']} path="/notes" component={NotesContainer} />

and then add the following code in your component, that renders on path='/'

Role based routing react redux
componentDidUpdate() {
  const { 
      children,  //Component to be rendered (HomeContainer if route = '/home')
      pathname: {location},  //location.pathname gives us the current url user is trying to hit. (with react router)
      profileId, //profileId required by profile page common to all user journeys.
      role } = this.props; 
  this.reRoute(role, this.props.children, location.pathname, ProfileId)
}

decideRoute(role, ProfileId) { //decide routes based on role
  if(role==='HR')
    return 'hrhome';
  else if(role==='KR')
    return 'home';
  else if(role==='USER'&&ProfileId)
    return 'profile/'+ProfileId;
  else
  return '/error';
}

isAuthorised(authorisedUsers, role) {
  return _.includes(authorisedUsers, role)
}

reRoute(role, children, path, ProfileId) {
  if(role===null && path!=='/') // user hit a different path without being authenticated first
  {
    hashHistory.replace('/');  //this is where we implemented login
    return;
  }
  let route = this.decideRoute(role, ProfileId)  //if role has already been fetched from the backend, use it to decide the landing page for each role.
  if(children)  // if we already are on one of the user journey screens ...
  {
    const authorisedUsers = children.props.route.authorisedUsers 
    if(!this.isAuthorised(authorisedUsers,role)) //... and the user is not allowed to view this page...
    hashHistory.replace(`/${route}/`);  //... redirect him to the home page corresponding to his role.
  }
  else
  hashHistory.replace(`/${route}/`);  // if the user has just logged in(still on / page or login page), and we know his role, redirect him to his home page.
}//if none of these holds true, user is allowed to go ahead and view the page

This essentially adds a gateway check that would work on all your containers and will direct you based on your role. Also, it wont allow you access if you somehow hit the wrong url.

1
votes

For anybody looking for a quick fix for it with an open source project, you can try to use react-permissible.

  • manage the visibility of particular components depending on users permissions
  • replace particular component when the user isn't permitted to see it
  • manage accessibility to particular view depending on users
    permissions
  • fire a callback when the user isn't allowed to go to access the component/route