React/reflux how to do proper async calls

5.3k views Asked by At

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.

3

There are 3 answers

4
damianmr On BEST ANSWER

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.

0
agmcleod On

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.

0
Björn Boxstart On

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.