Flux: async as an architecture

Hi there,

last time I told you about a handy Flux feature: you can handle the asyncronous code as sync one. So before we begin, I’ll describe the issue:

  • there is a big table, let it be 10k x 10k cells;
  • it’s possible to show only 10 x 10 cells on screen at once.
  • User can navigate in any direction of that big data canvas;
  • we can ask the server for a brief version of data (request takes <500ms);
    the full metadata package for 10 x 10 cells area is downloaded in 1.5-2s.

So once the user finished the navigation, we need to show at least something quickly then be ready to show all the metadata. Behind the UX we have to do two requests and handle the data respectively. How this could be done in Flux?

Concept of the data flow

Considering that x and y describe the position of visible window on the big data canvas. Assume that server API understands these pair well.
Schematically the data flow looks like this:
move() --> askForData(x, y, 'short') --> XHR('short').open()
After some time we have
xhrGotResponse() --> /* data */ --> Store --> View

Same sequence should happen for askForData(x, y, 'full'). The difference is, this sequence should be invoked not by move(). Assume that we run this request after we got data from previous one.

— Let’s invoke second XHR inside the callback of the first one!
Let’s not. This leads to the callback hell. Don’t you believe me? Look at that kind of code in a month.

Ok, what did you said about sync and async things?

Look at this version of action.js:

// Just emit the event for the Store.
Actions.update = function (data) {
  Dispatcher.dispatch({
    type: 'UPDATE',
    payload: data.payload,
    origin: data.origin
  });

};

// Run the request to the server.
// Invoke the `Actions.update` after the data are received.
Actions.askForData = function () {
  var state = Store.get();
  if (state.dataToLoad === 'brief') {
    XHR.load('/api/brief?x=' + state.x + '&y=' + state.y, function (response) {
      Actions.update({ payload: response, origin: 'brief' });
      Actions.askForData(); // @@label-02@@
    });
  }

  if (state.dataToLoad === 'full') {
    XHR.load('/api/full?x=' + state.x + '&y=' + state.y, function (response) {
      Action.update({ payload: response, origin: 'full' });
    });
  }
}


// Informs the Store about where exactly user arrived
Actions.move = function (x, y) {
  Dispatcher.dispatch({
    type: 'MOVE',
    x: x,
    y: y,
    timestamp: Date.now()
  });

  Actions.askForData(); // @@label-01@@
}

Now take the look at the store.js:

var _state = {/*...*/};

Store.get = function () {
  return _state;
};

Store.dispatchToken = Dispatcher.register(function (action) {
  switch (action.type) {
  case 'UPDATE':
    _state.data = action.payload; // apply converters
    Store.emit('CHANGE', _state.data);
    if (action.origin === 'full') {
      _state.dataToLoad = 'none';
    } else if (action.origin === 'brief') { // @@label-03@@
      _state.dataToLoad = 'full';
    }
    break;
  case 'MOVE':
    _state.x = action.x;
    _state.y = action.y;
    _state.timestamp = action.timestamp;
    _state.dataToLoad = 'brief'; // @@label-04@@
  }
});

Why so serious complicate?

Well, it’s not that complicate. It conforms our schematic data flow:

  • Action.move() --> /* update the store */ --> Action.askForData()
    Look for the @@label-01@@ in the code.
  • Action.askForData() --> /* decide which XHR to run */ --> XHR
  • After some time,
    XHR.response() --> /* update the store */ --> Store
    Store --> Store.emit('CHANGE') --> View
  • Now, we get back to the XHR.response() and run the Action.askForData() again! Check the @@label-02@@.
  • The main magic happens here: since Action.askForData() relies on Store’s .dataToLoad, we can control precisely the flow from the Store (@@label-03@@, @@label-04@@).
    • For example, we can add the timestamp check at these points.
    • We will invoke data loading (and eventually View updating) when the difference between two move events is more than, for instance, 200m.
    • Our goal is omitting intermediate requests during long scroll and showing the final data in the View.
      This also decreases the server load.

Now look at the code again. The Flux part looks synchronous (except of obviously asyncronous XHR things). However, a lot of this code is run in deferred way. By the way, there is no callback hell there.

Good luck with implementing one-directional data flow!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s