Avoiding event chains with asynchronous data dependencies
Asked Answered
F

2

13

The Facebook Flux dispatcher explicitly prohibits ActionCreators from dispatching other ActionCreators. This restriciton is probably a good idea since it prevents your application from creating event chains.

This however becomes an issue as soon as you have Stores containing data from asynchronous ActionCreators that depend on each other. If CategoryProductsStore depends on CategoryStore there doesn't seem to be a way to avoid event chains when without resorting to deferring the follow-up action.

Scenario 1: A store containing a list of products in a category needs to know from which category ID it should fetch products from.

var CategoryProductActions = {
  get: function(categoryId) {
    Dispatcher.handleViewAction({
      type: ActionTypes.LOAD_CATEGORY_PRODUCTS,
      categoryId: categoryId
    })

    ProductAPIUtils
      .getByCategoryId(categoryId)
      .then(CategoryProductActions.getComplete)
  },

  getComplete: function(products) {
    Dispatcher.handleServerAction({
      type: ActionTypes.LOAD_CATEGORY_PRODUCTS_COMPLETE,
      products: products
    })
  }
}

CategoryStore.dispatchToken = Dispatcher.register(function(payload) {
  var action = payload.action

  switch (action.type) {
    case ActionTypes.LOAD_CATEGORIES_COMPLETE:
      var category = action.categories[0]

      // Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
      CategoryProductActions.get(category.id)

      ...

Scenario 2: Another scenario is when a child component is mounted as the result of a Store change and its componentWillMount/componentWillReceiveProps attempts to fetch data via an asynchronous ActionCreator:

var Categories = React.createClass({
  componentWillMount() {
    CategoryStore.addChangeListener(this.onStoreChange)
  },

  onStoreChange: function() {
    this.setState({
      category: CategoryStore.getCurrent()
    })
  },

  render: function() {
    var category = this.state.category

    if (category) {
      var products = <CategoryProducts categoryId={category.id} />
    }

    return (
      <div>
        {products}
      </div>
    )
  }
})

var CategoryProducts = React.createClass({
  componentWillMount: function() {
    if (!CategoryProductStore.contains(this.props.categoryId)) {
      // Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
      CategoryProductActions.get(this.props.categoryId)
    }
  }
})

Are there ways to avoid this without resorting to defer?

Fatally answered 16/9, 2014 at 4:27 Comment(3)
For scenario #1, I put that kind of logic in the action creators themselves, so that stores only respond to changes in data. In the case that there is async logic, an action creator will sometimes dispatch multiple actions to the stores. I've ran into scenario #2, and either switch to DidMount (in the case of async data loading), or, occasionally, defer with setTimeout.Dolly
@BrandonTilley I've clarified both examples, in both cases the ActionCreator to fetch products in a category triggers an asynchronous API operation.Fatally
@SimenBrekken have you solved your problem? May you look here pls #32538068?Grip
S
3

Whenever you are retrieving the state of the application, you want to be retrieving that state directly from the Stores, with getter methods. Actions are objects that inform Stores. You could think of them as being like a request for a change in state. They should not return any data. They are not a mechanism by which you should be retrieving the application state, but rather merely changing it.

So in scenario 1, getCurrent(category.id) is something that should be defined on a Store.

In scenario 2, it sounds like you are running into an issue with the initialization of the Store's data. I usually handle this by (ideally) getting the data into the stores before rendering the root component. I do this in a bootstrapping module. Alternatively, if this absolutely needs to be async, you can create everything to work with a blank slate, and then re-render after the Stores respond to an INITIAL_LOAD action.

Speedometer answered 16/9, 2014 at 17:37 Comment(7)
I've clarified both examples, in both cases the ActionCreator to fetch products in a category triggers an asynchronous API operation. So in Scenario #1 I first fetch a list of categories from my API and then the products in that category, also via the API. As for Scenario #2 I do the same thing but only as the component is mounted and requires data. I don't know when the component will be mounted so getting data into the root component isn't possible.Fatally
Scenario 1: Where is the action creator that produces the action of type LOAD_CATEGORIES_COMPLETE ? It would seem that the call to the API should move to that action creator. It's not clear to me that LOAD_CATEGORY_PRODUCTS is useful.Speedometer
Scenario 2: Your view is managing the store's data. Let the store manage it's own data.Speedometer
I think there's been some confusion about where it's okay to call the API for more data. It is okay to do this in the store. The important thing is to simply make sure that when the data comes back, you then enter the flow with an action, rather than directly modifying the store. This ensures that the store is in control of itself, and any other stores can be informed of the same new data in the Flux way.Speedometer
Scenario #1: I'm currently returning a promise where CategoryProductActions.getComplete is the LOAD_CATEGORIES_COMPLETE action creator. Scenario #2: So you're saying I should ask the sore for products in the given category and the store itself calls the action creator?Fatally
@fishewebdev AS for LOAD_CATEGORY_PRODUCTS, it's used to notify the CategoryProductStore to clear the list of current products in preparation for a set of new ones from LOAD_CATEGORY_PRODUCTS_COMPLETE.Fatally
In Scenario #2 the categoryId comes from a router prop so I'm reluctant to store it twice.Fatally
P
0

For scenario 1:

I would dispatch new the action from the view itself, so a new action -> dispatcher -> store -> view cycle will trigger.

I can imagine that your view needs to retrieve the category list and also it has to show, by default, the list of products of the first category.

So that view will react to changes con CategoryStore first. Once the category list is loaded, trigger the new Action to get the products of the first category.

Now, this is the tricky part. If you do that in the change listener of the view, you will get an invariant exception, so here you have to wait for the payload of the first action to be completely processed.

One way to solve this is to use timeout on the change listener of the view. Something similar to what is explained here: https://groups.google.com/forum/#!topic/reactjs/1xR9esXX1X4 but instead of dispatching the action from the store, you would do it from the view.

function getCategoryProducts(id) {
setTimeout(() => {
    if (!AppDispatcher.isDispatching()) {
        CategoryProductActions.get(id);
    } else {
        getCategoryProducts(id);
    }
}, 3);
}

I know, it is horrible, but at least you won't have stores chaining actions or domain logic leaking to action creators. With this approach, the actions are "requested" from the views that actually need them.

The other option, which I haven't tried honestly, is to listen for the DOM event once the component with the list of categories is populated. In that moment, you dispatch the new action which will trigger a new "Flux" chain. I actually think this one is neater, but as said, I haven't tried yet.

Phthisis answered 2/6, 2015 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.