2

I recently started to learn ReactJS, but I'm getting confused for async calls.

Lets say I have a Login page with user/pass fields and login button. Component looks like:

var Login = React.createClass({

    getInitialState: function() {
        return {
            isLoggedIn: AuthStore.isLoggedIn()
        };
    },

    onLoginChange: function(loginState) {
        this.setState({
            isLoggedIn: loginState
        });
    },

    componentWillMount: function() {
        this.subscribe = AuthStore.listen(this.onLoginChange);
    },

    componentWillUnmount: function() {
        this.subscribe();
    },

    login: function(event) {
        event.preventDefault();
        var username = React.findDOMNode(this.refs.email).value;
        var password = React.findDOMNode(this.refs.password).value;
        AuthService.login(username, password).error(function(error) {
            console.log(error);
        });
    },

    render: function() {

        return (
                <form role="form">
                    <input type="text" ref="email" className="form-control" id="username" placeholder="Username" />
                    <input type="password" className="form-control" id="password" ref="password" placeholder="Password" />
                    <button type="submit" className="btn btn-default" onClick={this.login}>Submit</button>
                </form>
        );
    }
});

AuthService looks like:

module.exports = {
    login: function(email, password) {
        return JQuery.post('/api/auth/local/', {
            email: email,
            password: password
        }).success(this.sync.bind(this));
    },

    sync: function(obj) {
        this.syncUser(obj.token);
    },

    syncUser: function(jwt) {
        return JQuery.ajax({
            url: '/api/users/me',
            type: "GET",
            headers: {
                Authorization: 'Bearer ' + jwt
            },
            dataType: "json"
        }).success(function(data) {
            AuthActions.syncUserData(data, jwt);
        });
    }
};

Actions:

var AuthActions = Reflux.createActions([
  'loginSuccess',
  'logoutSuccess',
  'syncUserData'
]);

module.exports = AuthActions;

And store:

var AuthStore = Reflux.createStore({
    listenables: [AuthActions],

    init: function() {
        this.user = null;
        this.jwt = null;
    },

    onSyncUserData: function(user, jwt) {
        console.log(user, jwt);
        this.user = user;
        this.jwt = jwt;
        localStorage.setItem(TOKEN_KEY, jwt);
        this.trigger(user);
    },

    isLoggedIn: function() {
        return !!this.user;
    },

    getUser: function() {
        return this.user;
    },

    getToken: function() {
        return this.jwt;
    }
});

So when I click the login button the flow is the following:

Component -> AuthService -> AuthActions -> AuthStore

I'm directly calling AuthService with AuthService.login.

My question is I'm I doing it right?

Should I use action preEmit and do:

var ProductAPI = require('./ProductAPI')
var ProductActions = Reflux.createActions({
  'load',
  'loadComplete',
  'loadError'
})

ProductActions.load.preEmit = function () {
     ProductAPI.load()
          .then(ProductActions.loadComplete)
          .catch(ProductActions.loadError)
}

The problem is the preEmit is that it makes the callback to component more complex. I would like to learn the right way and find where to place the backend calls with ReactJS/Reflux stack.

Dmitry Shvedov
  • 3,169
  • 4
  • 39
  • 51
Deepsy
  • 3,769
  • 7
  • 39
  • 71

3 Answers3

6

I am using Reflux as well and I use a different approach for async calls.

In vanilla Flux, the async calls are put in the actions.

enter image description here

But in Reflux, the async code works best in stores (at least in my humble opinion):

enter image description here

So, in your case in particular, I would create an Action called 'login' which will be triggered by the component and handled by a store which will start the login process. Once the handshaking ends, the store will set a new state in the component that lets it know the user is logged in. In the meantime (while this.state.currentUser == null, for example) the component may display a loading indicator.

damianmr
  • 2,511
  • 1
  • 15
  • 15
  • Thanks for the answer. What do you think about http://stackoverflow.com/questions/28012356/proper-way-to-initialize-data this approach `actions.init.listen`? Because I did a research and many people are doing it your way, but also many people are doing it in the action. So I'm confused and can't decide which pattern to use. – Deepsy Jun 13 '15 at 22:26
  • Well there are differences in the way people are using it because for Reflux there's no "correct way". It really depends on how you want to model your app. If I needed to have an action like 'init' I would still make the requests and also put all the init code in a store. Maybe I could even create an "InitializationStore". – damianmr Jun 14 '15 at 22:13
  • Also, creating actions that "do many things" may be adding coupling on what the action really means. An action like "login", for instance, with an AJAX call should not be called more than once per second, but if you have all your ajax code in there then you would have to save some state for the action so that it does not make the ajax call more than once until the first one completes. If you had the API call in a store, it could easily handle this. Plus, other stores may be interested into knowing that the user requested to login, regardless the success of the API call. – damianmr Jun 14 '15 at 22:23
  • Yup. It just makes sense to put backend access into stores. See also: http://stackoverflow.com/a/28101529/1775026 – wle8300.com Jul 31 '15 at 23:53
1

For Reflux you should really take a look at https://github.com/spoike/refluxjs#asynchronous-actions.

The short version of what is described over there is:

  1. Do not use the PreEmit hook

  2. Do use asynchronous actions

var MyActions = Reflux.createActions({
  "doThis" : { asyncResult: true },
  "doThat" : { asyncResult: true }
});

This will not only create the 'makeRequest' action, but also the 'doThis.completed', 'doThat.completed', 'doThis.failed' and 'doThat.failed' actions.

  1. (Optionally, but preferred) use promises to call the actions
MyActions.doThis.triggerPromise(myParam)
  .then(function() {
    // do something
    ...
    // call the 'completed' child
    MyActions.doThis.completed()
  }.bind(this))
  .catch(function(error) {
    // call failed action child
    MyActions.doThis.failed(error);
  });

We recently rewrote all our actions and 'preEmit' hooks to this pattern and do like the results and resulting code.

Mo.
  • 26,306
  • 36
  • 159
  • 225
Björn Boxstart
  • 1,098
  • 1
  • 12
  • 25
0

I also found async with reflux kinda confusing. With raw flux from facebook, i would do something like this:

var ItemActions = {
  createItem: function (data) {
    $.post("/projects/" + data.project_id + "/items.json", { item: { title: data.title, project_id: data.project_id } }).done(function (itemResData) {
      AppDispatcher.handleViewAction({
        actionType: ItemConstants.ITEM_CREATE,
        item: itemResData
      });
    }).fail(function (jqXHR) {
      AppDispatcher.handleViewAction({
        actionType: ItemConstants.ITEM_CREATE_FAIL,
        errors: jqXHR.responseJSON.errors
      });
    });
  }
};

So the action does the ajax request, and invokes the dispatcher when done. I wasn't big on the preEmit pattern either, so i would just use the handler on the store instead:

var Actions = Reflux.createActions([
  "fetchData"
]);

var Store = Reflux.createStore({
  listenables: [Actions],

  init() {
    this.listenTo(Actions.fetchData, this.fetchData);
  },

  fetchData() {
    $.get("http://api.com/thedata.json")
      .done((data) => {
        // do stuff
      });
  }
});

I'm not big on doing it from the store, but given how reflux abstracts the actions away, and will consistently fire the listenTo callback, i'm fine with it. A bit easier to reason how i also set call back data into the store. Still keeps it unidirectional.

agmcleod
  • 13,321
  • 13
  • 57
  • 96