Fork me on GitHub
Fluxxor

Dealing with Asynchronous Data

In any decently complex JavaScript web application, you'll likely need to fetch data from an external source, and this means dealing with asynchronous data fetching. It's not always very obvious how to structure this activity in a flux application.

The short answer is: in order to ensure that all the stores in your application have a chance to respond to the successful (or unsuccessful) loading of asynchronous data, you should fire additional actions from your asynchronous handlers to indicate when loading fails or succeeds. Let's look at an example.

Note: The code for this example is available in the GitHub repository. Note that Faker and Lo-Dash are loaded via CDN in index.html, and the application makes use of Lo-Dash methods when appropriate.

This application simulates an asynchronous API using setTimeout that performs two operations:

  1. Gets a list of catch phrases for your new startup
  2. Allows you to submit new catch phrases to the database

To allow for testing error conditions, 50% of the time submitting a new phrase will fail with an error. Here's the app running:

Let's walk through building the app together.

The Client API

Since we're using a fake asynchronous API, we just have a dumb client object that waits for a couple seconds before "responding." It has two methods; load returns 10 buzzwords, and submit takes a buzzword and either reports a success or error (to simulate submitting a suggestion to a server).

var BuzzwordClient = {
  load: function(success, failure) {
    setTimeout(function() {
      success(_.range(10).map(Faker.Company.catchPhrase));
    }, 1000);
  },

  submit: function(word, success, failure) {
    setTimeout(function() {
      if (Math.random() > 0.5) {
        success(word);
      } else {
        failure("Failed to " + Faker.Company.bs());
      }
    }, Math.random() * 1000 + 500);
  }
};

Actions

There are two application intents we'll capture via actions; one is to load the initial set of buzzwords, and the other is to submit a new buzzword. However, since we're dealing with asynchronous operations, we'll also define a SUCCESS and FAIL action type for each.

var constants = {
  LOAD_BUZZ: "LOAD_BUZZ",
  LOAD_BUZZ_SUCCESS: "LOAD_BUZZ_SUCCESS",
  LOAD_BUZZ_FAIL: "LOAD_BUZZ_FAIL",

  ADD_BUZZ: "ADD_BUZZ",
  ADD_BUZZ_SUCCESS: "ADD_BUZZ_SUCCESS",
  ADD_BUZZ_FAIL: "ADD_BUZZ_FAIL"
};

The actions themselves will immediately dispatch the LOAD_BUZZ or ADD_BUZZ action types so that any stores that want to optimisically update the UI can do so. They will then delegate to the BuzzwordClient and dispatch the appropriate success or failure action type depending on how it responds.

We also generate a temporary client-side ID that we can use to track a specific word across the asynchronous operations, and include it in part of the payload when adding a new buzzword.

var actions = {
  loadBuzz: function() {
    this.dispatch(constants.LOAD_BUZZ);

    BuzzwordClient.load(function(words) {
      this.dispatch(constants.LOAD_BUZZ_SUCCESS, {words: words});
    }.bind(this), function(error) {
      this.dispatch(constants.LOAD_BUZZ_FAIL, {error: error});
    }.bind(this));
  },

  addBuzz: function(word) {
    var id = _.uniqueId();
    this.dispatch(constants.ADD_BUZZ, {id: id, word: word});

    BuzzwordClient.submit(word, function() {
      this.dispatch(constants.ADD_BUZZ_SUCCESS, {id: id});
    }.bind(this), function(error) {
      this.dispatch(constants.ADD_BUZZ_FAIL, {id: id, error: error});
    }.bind(this));
  }
};

From here on out, our app will look pretty similar to any other.

Store

Our store now needs to watch for the various actions and update appropriately. While the code looks a little more verbose than its synchronous counterparts (because, well, it is), it keeps the flow of data throughout the system explicit.

var BuzzwordStore = Fluxxor.createStore({
  initialize: function() {
    this.loading = false;
    this.error = null;
    this.words = {};

    this.bindActions(
      constants.LOAD_BUZZ, this.onLoadBuzz,
      constants.LOAD_BUZZ_SUCCESS, this.onLoadBuzzSuccess,
      constants.LOAD_BUZZ_FAIL, this.onLoadBuzzFail,

      constants.ADD_BUZZ, this.onAddBuzz,
      constants.ADD_BUZZ_SUCCESS, this.onAddBuzzSuccess,
      constants.ADD_BUZZ_FAIL, this.onAddBuzzFail
    );
  },

  onLoadBuzz: function() {
    this.loading = true;
    this.emit("change");
  },

  onLoadBuzzSuccess: function(payload) {
    this.loading = false;
    this.error = null;

    this.words = payload.words.reduce(function(acc, word) {
      var clientId = _.uniqueId();
      acc[clientId] = {id: clientId, word: word, status: "OK"};
      return acc;
    }, {});
    this.emit("change");
  },

  onLoadBuzzFail: function(payload) {
    this.loading = false;
    this.error = payload.error;
    this.emit("change");
  },

  onAddBuzz: function(payload) {
    var word = {id: payload.id, word: payload.word, status: "ADDING"};
    this.words[payload.id] = word;
    this.emit("change");
  },

  onAddBuzzSuccess: function(payload) {
    this.words[payload.id].status = "OK";
    this.emit("change");
  },

  onAddBuzzFail: function(payload) {
    this.words[payload.id].status = "ERROR";
    this.words[payload.id].error = payload.error;
    this.emit("change");
  }
});

The BuzzwordStore code is pretty straightforward; the most interesting portion is probably the last three methods. Notice we optimistically add the submitted word to the store in onAddBuzz and mark it as "ADDING" since we don't know if it will succeed or not. Later, in onAddBuzzSuccess and onAddBuzzFail, we track down the word in question and update its status accordingly. In another app, we might present an alert to the user upon failure, or remove the word from the store completely.

The UI

For completeness, here is the UI for this application. Note that the Word component looks at the status for the word object to determine how to display it to the user. Also notice how the application can render itself even when the store is empty; this is an important property for flux apps that need to load data asynchronously.

var stores = {
  BuzzwordStore: new BuzzwordStore()
};

var flux = new Fluxxor.Flux(stores, actions);

flux.on("dispatch", function(type, payload) {
  if (console && console.log) {
    console.log("[Dispatch]", type, payload);
  }
});

var FluxMixin = Fluxxor.FluxMixin(React),
    StoreWatchMixin = Fluxxor.StoreWatchMixin;

var Application = React.createClass({
  mixins: [FluxMixin, StoreWatchMixin("BuzzwordStore")],

  getInitialState: function() {
    return {
      suggestBuzzword: ""
    };
  },

  getStateFromFlux: function() {
    var store = this.getFlux().store("BuzzwordStore");
    return {
      loading: store.loading,
      error: store.error,
      words: _.values(store.words)
    };
  },

  render: function() {
    return (
      <div>
        <h1>All the Buzzwords</h1>
        {this.state.error ? "Error loading data" : null}
        <ul style={{lineHeight: "1.3em", minHeight: "13em"}}>
          {this.state.loading ? <li>Loading...</li> : null}
          {this.state.words.map(function(word) {
            return <Word key={word.id} word={word} />;
          })}
        </ul>
        <h2>Suggest a New Buzzword</h2>
        <form onSubmit={this.handleSubmitForm}>
          <input type="text" value={this.state.suggestBuzzword}
                 onChange={this.handleSuggestedWordChange} />
          <input type="submit" value="Add" />
        </form>
      </div>
    );
  },

  componentDidMount: function() {
    this.getFlux().actions.loadBuzz();
  },

  handleSuggestedWordChange: function(e) {
    this.setState({suggestBuzzword: e.target.value});
  },

  handleSubmitForm: function(e) {
    e.preventDefault();
    if (this.state.suggestBuzzword.trim()) {
      this.getFlux().actions.addBuzz(this.state.suggestBuzzword);
      this.setState({suggestBuzzword: ""});
    }
  }
});

var Word = React.createClass({
  render: function() {
    var statusText, statusStyle = {};
    switch(this.props.word.status) {
    case "OK":
      statusText = "";
      break;
    case "ADDING":
      statusText = "adding...";
      statusStyle = { color: "#ccc" };
      break;
    case "ERROR":
      statusText = "error: " + this.props.word.error;
      statusStyle = { color: "red" };
      break;
    }

    return (
      <li key={this.props.word.word}>
        {this.props.word.word} <span style={statusStyle}>{statusText}</span>
      </li>
    );
  }
});

And that's it! Be sure to check out the full example in the GitHub repo.


See a typo? Something still not clear? Report an issue on GitHub.