9

I have been playing with React/Flux, and I am having trouble wrapping my head around the 'Flux Way' for handling permission-sensitive actions.

Overarching question: When a non-logged in visitor attempts an action that requires he/she to be logged in, what is the Flux way of (a) checking if a user is logged in, (b) starting the login flow, (c) finishing the action on success?

Take an example of a forum app, that requires users to be logged in to post:

We have a comment form component (mostly taken from FB's React tut) as such:

var CommentForm = React.createClass({
  handleSubmit: function ( e ) {
    e.preventDefault();

    // get data
    commentData = {
      content: this.refs.content.getDOMNode().value.trim()
    };

    this.props.onCommentSubmit( commentData );
    this.resetForm();
  },

  resetForm: function () {
    this.refs.content.getDOMNode().value = '';
  },

  render: function () {
    return (
      <form className="comment-form" id="comment-form" onSubmit={ this.handleSubmit }>
        <textarea name="comment[content]" placeholder="What's on your mind?" ref="content"></textarea>
        <button className="post-comment button" type="submit">Post</button>  
      </form>
    )
  }
});

A comments store (abbreviated)

var CHANGE_EVENT = 'change';
var _comments = {};

function createComment ( data ) {
  // post to server
}

var CommentStore = React.addons.update(EventEmitter.prototype, {$merge: {

  // omitted methods

  dispatcherIndex: AppDispatcher.register(function(payload) {
    var action = payload.action;
    var text;

    switch(action.actionType) {
      case CommentConstants.ADD_COMMENT:
        AppDispatcher.waitFor([SessionStore.dispatchToken])
        createComment(action.data);
        break;
    }

    return true;
  })
}});

And a store for handling sessions (also abbreviated):

var CHANGE_EVENT = 'change';

function ensureCurrentUser () {
  if ( !SessionStore.currentUser() ) {
    app.router.navigate('/login');
  }
}

var SessionStore = React.addons.update(EventEmitter.prototype, {$merge: {

  // omitted code

  currentUser: function () {
    return app.context.current_user;
  },

  dispatchToken: AppDispatcher.register(function ( payload ) {
    var action = payload.action;

    switch(action.actionType) {
      case CommentConstants.ADD_COMMENT:
        ensureCurrentUser();
        break;
    }

    return true;
  })
}});

My initial thought was that this is a case for Flux's waitFor() method. However, the implementation above fails, as waitFor closes the dependency loop as soon as SessionStore.dispatchToken is set (as soon as ensureCurrentUser returns).

Is this a case where the payload should be passed into ensureCurrentUser, and then into the route handler for /login? What is the Flux way of handling these types of flows?

Thanks in advance :)

mattmattmatt
  • 965
  • 3
  • 15
  • 29

3 Answers3

7

As FakeRainBrigand suggested in his answer, you'd want to just check that the user has a valid session before creating a comment by first retrieving that value from the SessionStore:

case CommentConstants.ADD_COMMENT:
  if (SessionStore.getUser()) {
   createComment(action.data);
  }
  break;

But to preserve the comment so that it gets created after the user logs in, I would create a collection of pending comments in the CommentStore, and then later, in a callback to the login verification and session creation, dispatch a new action to inform the CommentStore that the user has now logged in. Then the CommentStore can respond to that by creating real comments out of the pending ones.

fisherwebdev
  • 12,658
  • 4
  • 28
  • 27
  • 2
    Just some other, unrelated issues with the code in the question: Keeping state in the DOM is a code smell in React. This line: `this.refs.content.getDOMNode().value.trim()` should be replaced by `this.state.content` -- you should be maintaining the state of the textarea in `this.state`. Also note that your ContentStore's `dispatcherIndex` should be probably be renamed `dispatchToken` for consistency with the SessionStore. – fisherwebdev Oct 12 '14 at 20:45
  • 1
    Thanks for the additional tips! The `this.refs.content.getDOMNode().value.trim()` was actually taken directly from [Facebook's React tutorial](http://facebook.github.io/react/docs/tutorial.html), where it is used in the `CommentForm` component. Should I instead be updating state whenever the user changes the comment content, and then simply pulling from state on submit? – mattmattmatt Oct 13 '14 at 00:21
  • 3
    Hmmm, I wish that wasn't up there on the React site, because it's in contrast to this page: http://facebook.github.io/react/docs/forms.html -- this is closer to how I work with inputs, where I maintain the value in the local component's `this.state`. See also the Flux-TodoMVC app for a more complete example. – fisherwebdev Oct 13 '14 at 17:50
  • @fisherwebdev: Re: "Then the CommentStore can respond to that by creating real comments out of the pending ones" -- how does this happen in Flux? I thought a Store should not be initiating network activity (at least if you keep your stores as dumb data repositories as I do per http://stackoverflow.com/questions/25630611/should-flux-stores-or-actions-or-both-touch-external-services). Should the store call back to an Action Creator? But I thought Stores shouldn't initiate Actions? Thanks! – Lane Rettig Jan 21 '16 at 17:06
  • @LaneRettig Stores are not dumb data repositories. They are where all of the logic of the application should reside. There is nothing wrong with initiating network activity from a store. Some people prefer to initiate from action creators, some people prefer to initiate from stores. It's mostly a matter of taste. The important thing about async communication with the server is that the response needs to dispatch a new action, not be handled directly in the store. – fisherwebdev Feb 12 '16 at 23:18
3

The simplest would be just asking SessionStore if there's a session.

 case CommentConstants.ADD_COMMENT:
    if (SessionStore.getUser()) {
       createComment(action.data);
    }
    break;

Of course, this just saves you a server request. The behavior shouldn't be any different than always calling createComment(action.data).

Is this a case where the payload should be passed into ensureCurrentUser, and then into the route handler for /login? What is the Flux way of handling these types of flows?

For this, you might want to have an event emitter that emits a 'login' event.

 case CommentConstants.ADD_COMMENT:
    if (SessionStore.getUser()) {
       createComment(action.data);
    }
    else {
       someEventEmitter.one('login', function(){
           createComment(action.data);
       });
       someEventEmitter.emit('loginRequired');
    }
    break;

And when loginRequired is emitted, if there isn't a user logged in, the login view would be presented.

Brigand
  • 84,529
  • 20
  • 165
  • 173
  • beat me to it by 20 sec ;D – Mike Driver Oct 12 '14 at 19:59
  • heh somehow I missed the most important part of the question at first, so perhaps I should have spent an extra 20 seconds reading it :-) – Brigand Oct 12 '14 at 20:01
  • 1
    FakeRainBrigand is correct here about asking the SessionStore for the session with `SessionStore.getUser()`. Not sure I entirely agree with the EventEmitter stuff, as it seems like it turns the app into a pubsub architecture, and away from Flux. If you are trying to preserve the comment so that it gets created after the user logs in, I would create a collection of pending comments in the CommentStore, and then later dispatch an action to inform the CommentStore that the user has now logged in. Then the store can respond to that by creating real comments out of the pending ones. – fisherwebdev Oct 12 '14 at 20:11
  • Yeah, I tend to lean away from pure flux. If fisherwebdev creates an answer, please accept that. – Brigand Oct 12 '14 at 20:13
  • I think your answer + these comments are acceptable enough :) – fisherwebdev Oct 12 '14 at 20:14
  • Hey thanks for the answer and comments! I would prefer to accept an answer that is specific to the Flux architecture, sticking the paradigm of one-way data flows, rather than introducing a global events object. I'm happy to accept this answer + an edit including fisherwebdev's suggestion, or a new answer. Thanks again! – mattmattmatt Oct 12 '14 at 20:29
3

It should be noted that there's an example called auth-flow in react-router repo, that shines light on how this could be done. Although, I'm still trying to make it work for me it is useful to add here.

rohay
  • 41
  • 2