Skip to content

Instantly share code, notes, and snippets.

@jhorneman
Last active August 22, 2016 02:18
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jhorneman/fe79fb67da2c74656436 to your computer and use it in GitHub Desktop.
Save jhorneman/fe79fb67da2c74656436 to your computer and use it in GitHub Desktop.
Thoughts on where to do async data loading in Flux

Async data loading in Flux

I've been working with Flux a lot recently, and one of the questions I've been struggling with is in which part of the Flux cycle to put my asynchronous data requests.

Here are some different opinions:

The diagram

The famous Flux diagram puts them in the action creators.

The chat example

The Chat example only simulates data requests and I can't tell "recommended Flux style" from "here's how we simulated a server".

Pete Hunt

Pete Hunt claims here that:

It's still not clear where in Flux you load your data from the server

Dan Abramov

Dan Abramov claims:

Store must not fire actions or cause any side effects like AJAX calls

Reto Schläpfer

This blog post is interesting, because it discusses two different approaches.

The first approach is to have the Stores do the API calls:

We would dispatch an action which a Store then reacts on by providing a callback to the Web Api that will eventually change the state of a Store.

Mr. Schläpfer later concluded:

Our way of doing async requests sucks. Making the Flux-Stores directly call the api layer and provide a callback is a bad idea in the long run: It's just too hard to reason about the data flow.

And changed it to:

The smarter way is to call the Web Api directly from an Action Creator and then make the Api dispatch an event with the request result as a payload. The Store(s) can choose to listen on those request actions and change their state accordingly.

Explaining why this is superior:

There should be only one channel for all state changes: The Dispatcher. This makes debugging easy because it just requires a single console.log in the dispatcher to observe every single state change trigger.

Asynchronously executed callbacks should not leak into Stores. The consequences of it are just to hard to fully foresee. This leads to elusive bugs. Stores should only execute synchronous code. Otherwise they are too hard to understand.

Avoiding actions firing other actions makes your app simple. We use the newest Dispatcher implementation from Facebook that does not allow a new dispatch while dispatching. It forces you to do things right.

My first approach

OK. So I took that approach as well and put my async requests in the action creators.

Then I ran into a problem. And I forgot what that problem is :( Maybe it's related to my hierarchical data. It was probably trying to dispatch actions while another action was ongoing.

(Not remembering my problem arguably defeats the purpose of writing this, but I'm soldiering on anyway.)

My second approach

It turns out in the comments that the author of the blog post had a similar problem:

I've stumbled into the same issue quite qickly as well. My solution for this is to defer the emitChanges of the Stores to the end of the stack:

emitChange: function() {
process.nextTick(() => this.emit(CHANGE_EVENT));
},

It turns out Bill Fisher has an interesting opinion on where to do async loading:

You can call for data in either the action creators or the stores. The important thing is to not handle the response directly, but to create an action in the error/success callback. Handling the response directly in the store leads to a more brittle design.

I also read somewhere, but I can no longer find where, that he frowned upon using process.nextTick() in cases like this.

I decided to do put my requests in stores and dispatch a new action when the promise resolves. So I have a lot of code like this:

if (action.type === ActionTypes.SELECT_RECORDING) {
    ServerAPI.getFrameData(RecordingStore.getSelectedID())
    .done(function(response) {
        RecordingFrameActions.receiveAll(response);
    });
}

It works great, except when you have a cache in your API module and your async promise resolves instantly, and so you end up dispatching the 'Received' action while you're still handling the action that caused you to require the data in the first place. I use process.nextTick() for that.

More detail

To give a bit more detail, my interaction loop is like this:

  • The user interacts with a component, e.g. a drop-down list.
  • The component's event handler calls an action creator, which dispatches (for example) a SELECT_RECORDING action.
  • The RecordingFrameStore (!) reacts to this action and makes an AJAX call (through an API module).
  • When the AJAX promise resolves, the RecordingFrameStore calls another action creator, which dispatches a RECEIVE_FRAME_DATA action.
  • The RecordingFrameStore reacts to this action, updates its internal state, and emits a change event.
  • Components (container components) react to this event, re-get data from stores, and call setState.
  • React re-renders these components (or not).

Conclusion

  • My solution works quite well when viewed in isolation.

  • Error management and let's call it pending management are not done yet, so perhaps that will force me to change my approach. I detect errors in the API module and display them there. In my use case I work with a local server 99% of the time, so I don't need to show the user I'm loading data.It works well enough for now, but could be cleaner.

  • I do have other architectural issues on a larger level - I have a ton of stores that are all much more interconnected than I would like. This might be related to where I do my data requests - I don't know yet.

  • In general, it seems this is not super-clear to a lot of people. I don't quite know what to make of that.

Update

Dan Abramov just pointed out I can't have record/replay if I make AJAX calls from Stores.

It turns out "AJAX calls in Stores" is slightly ambiguous: it can mean in a getter or other Store method, as well as in the dispatch callback. Which is discouraged, apparently.

And of course Dan is right: I cannot just take an array of actions and replay them if one of those actions will make a Store make another AJAX call. I could not make those calls if I know I am replaying, but that is obviously less elegant.

@gaearon
Copy link

gaearon commented May 30, 2015

The solution is much simpler: you don't need nested calls and stores shouldn't trigger actions. Instead keep your data fully normalized like in a database and reference entities by unique IDs. When selection changes, don't clear the stores — only update the selected ID.

See also: https://gist.github.com/jhorneman/c35f48d8c66d1c96e4db#comment-1463805

@jhollingworth
Copy link

You might be interested by Marty's approach to fetching data. AJAX queries are indirectly invoked from stores via queries (essentially action creators for getting data vs. writing data). Record/replay is still achievable with this approach plus enables some interesting use cases around isomorphism

@jhollingworth
Copy link

alt has actually recently adopted the same pattern

@tomkis
Copy link

tomkis commented May 30, 2015

I have been always struggling with API calls in Flux. I believed the only proper solution was to have the API calls in ActionCreator. However, this approach has few disadvantages:

  • Some API calls might need state to parametrise the HTTP request
  • Sometimes you need to perform a logic to determine which API call should be called and believe it or not this is a business logic

API calls are inevitably part of your business logic and may depend on state, therefore it makes a huge sense to keep them where your business logic should be (Stores).

I've stumbled into the same issue quite qickly as well. My solution for this is to defer the emitChanges of the Stores to the end of the stack:

I can do code review for any Flux application in just a few minutes by hitting git grep "_.defer" (or any alternative) to find out if there is anything conceptually wrong about design. Using defer frequently is always a sign that something goes wrong.

I also read somewhere, but I can no longer find where, that he frowned upon using process.nextTick() in cases like this.

You don't need to worry about this as API callbacks are always deferred to the end of the stack. In fact, it's not even cascading action call because it's async. Though it's definitely a side effect originated in your stores.

It works great, except when you have a cache in your API module and your async promise resolves instantly, and so you end up dispatching the 'Received' action while you're still handling the action that caused you to require the data in the first place. I use process.nextTick() for that.

In my opinion, to keep implementation consistent, you should have your cache hit implementation async (deferred) and you will avoid the problem with process.nextTick

Dan Abramov just pointed out I can't have record/replay if I make AJAX calls from Stores.

I don't think there is any problem with record/replay. Every API response should emit action which holds the response in its payload. Of course your Stores may cause API call side-effects which you can easily avoid by mocking them when re-playing.

@briandipalma
Copy link

Promises shouldn't resolve sync, you have a bad implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment