How to avoid dispatching in the middle of a dispatch
Asked Answered
M

5

21

Within my Flux architected React application I am retrieving data from a store, and would like to create an action to request that information if it does not exist. However I am running into an error where the dispatcher is already dispatching.

My desired code is something like:

getAll: function(options) {
  options = options || {};
  var key = JSON.stringify(options);
  var ratings = _data.ratings[key];

  if (!ratings) {
    RatingActions.fetchAll(options);
  }

  return ratings || [];
}

However intermittently fails when the dispatcher is already dispatching an action, with the message Invariant Violation: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.. I am often making requests in response to a change in application state (eg date range). My component where I make the request, in response to a change event from the AppStore has the following:

getStateFromStores: function() {
  var dateOptions = {
    startDate: AppStore.getStartISOString(),
    endDate: AppStore.getEndISOString()
  };

  return {
    ratings: RatingStore.getAll(dateOptions),
  };
},

I am aware that event chaining is a Flux antipattern, but I am unsure what architecture is better for retrieving data when it does not yet exist. Currently I am using this terrible hack:

getAll: function(options) {
  options = options || {};
  var key = JSON.stringify(options);
  var ratings = _data.ratings[key];

  if (!ratings) {
    setTimeout(function() {
      if (!RatingActions.dispatcher.isDispatching()) {
        RatingActions.fetchAll(options);
      }
    }, 0);
  }

  return ratings || [];
},

What would be a better architecture, that avoids event chaining or the dispatcher error? Is this really event chaining? I just want to change the data based on the parameters the application has set.

Thanks!

Merriemerrielle answered 20/5, 2015 at 18:0 Comment(3)
This is one of reasons why I switched from Flux to Redux.Simple
@ian-walker-sperber This question has been viewed a lot. Any chance you could chose the correct answer?Cosby
@RyanRho How do you deal with messy code?Barty
C
13

You can use Flux waitFor() function instead of a setTimeout

For example you have 2 stores registered to the same dispatcher and have one store waitFor the other store to process the action first then the one waiting can update after and dispatch the change event. See Flux docs example

Cosby answered 22/5, 2015 at 13:0 Comment(0)
M
9

My particular error was occurring because my stores emitted their change event during the action dispatch, while it was still cycling through the listeners. This meant any listeners (ie components) that then triggered an action due to a data change in the store would interrupt the dispatch. I fixed it by emitting the change event after the dispatch had completed.

So this:

this.emit(CHANGE_EVENT);

Became

var self = this;

setTimeout(function() { // Run after dispatcher has finished
  self.emit(CHANGE_EVENT);
}, 0);

Still a little hacky (will probably rewrite so doesn't require a setTimeout). Open to solutions that address the architectural problem, rather than this implementation detail.

Merriemerrielle answered 21/5, 2015 at 14:28 Comment(3)
I ran into the same problem and also had to do this. Is there another way to solve this? Indeed a bit hacky. How did you solve this in the end?Sickener
@JurgenVandw On this project I stuck with my setTimeout solution since it was near completion, but I would suggest trying waitFor() as @Cosby suggested. On subsequent projects I have been using redux with thunk and have had a much easier time handling this and similar situations :)Merriemerrielle
The setTimeout is a nasty trick. Today I am banging around the same issue after implementing a new store: all the previous store use your "problematic" implementation and never had an issue. Might be an erroneous implementation around somewhereBarty
H
5

The reason you get a dispatch in the middle of a previous dispatch, is that your store dispatches an action (invokes an action creator) synchronously in the handler for another action. The dispatcher is technically dispatching until all its registered callbacks have been executed. So, if you dispatch a new action from either of the registered callbacks, you'll get that error.

However, if you do some async work, e.g. make an ajax request, you can still dispatch an action in the ajax callbacks, or the async callback generally. This works, because as soon as the async function has been invoked, it per definition immediately continues the execution of the function and puts the callback on the event queue.

As pointed out by Amida and in the comments of that answer, it's a matter of choice whether to make ajax requests from the store in response to an action, or whether to do it in the store. The key is that a store should only mutate its state in response to an action, not in an ajax/async callback.

In your particular case, this would be exemplified by something like this for your store's registered callback, if you prefer to make the ajax calls from the store:

onGetAll: function(options) {
    // ...do some work

    request(ajaxOptions) // example for some promise-based ajax lib
        .then(function(data) {
             getAllSuccessAction(data); // run after dispatch
        })
        .error(function(data) {
             getAllFailedAction(data); // run after dispatch
        });

     // this will be immediately run during getAllAction dispatch
     return this.state[options];
},
onGetAllSuccess: function(data) {
    // update state or something and then trigger change event, or whatever
},    
onGetAllFailed: function(data) {
    // handle failure somehow
}

Or you can just put the ajax call in your action creator and dispatch the "success/failed" actions from there.

Horseplay answered 16/7, 2015 at 9:35 Comment(0)
C
3

you can user the "defer" option in the dispatcher. In your case it would be like:

RatingActions.fetchAll.defer(options);
Candelaria answered 10/10, 2016 at 22:27 Comment(0)
A
0

In my case, I fetch data through the actions/actions creators. The store is only a dump place that receives the payload of an action.

This means that I would "fetchall" in an action and then pass the result to the store which will do whatever with it and then emit a change event.

Some people consider using stores like me, others think like you. Some people at Facebook uses "my" approach:

https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/utils/ChatWebAPIUtils.js#L51

I think it would probably avoid the dispatch problem treating your stores like this, but I may be wrong.

An interesting discussion is this one: https://groups.google.com/forum/#!topic/reactjs/jBPHH4Q-8Sc

where Jing Chen (Facebook engineer) explains what she thinks about how to use stores.

Alyse answered 20/5, 2015 at 18:35 Comment(6)
The data fetching still happens in an action, we simply allow the store to kick off an action if the data does not yet exist. In the component we don't know if we have the correct data locally or not. Even if the action was placed in the component, it would have to go in the same method in response to a store change (since the date changed), and so would still be in the middle of a dispatch.Merriemerrielle
Your store should not fire actions. Have a look at these guidelines. pbs.twimg.com/media/CDyAQ-BW0AESYP1.png:large I haven't been able to find the author.Alyse
According to Bill Fisher, a maintainer of Facebook's Flux repo, that is a misconception. See his comment at #25861142 , where he explicitly states that it is ok (I thought it wasn't for a long time too). Though still just an architectural choiceMerriemerrielle
Maybe, that's what Jing Chen says too in the conversation I have linked to, but I think that if you didn't do that you would not have the problem that lead you here. I've never faced it. At least I think you should try to see if that solves your problem.Alyse
There's some subtlety here. What Bill Fisher is saying is that it's ok for stores to do async work in response to an action (most likely ajax requests), and then trigger an action in the async callback. That will not dispatch in the middle of a dispatch, because after the ajax request has been made the execution will continue synchronously until the dispatch is done. So stores can't dispatch new events in the synchronous response to an action, but it can do it async. If you choose. I prefer to do async work in action creators.Horseplay
I treat my action creators as some hub/service call center. Meaning that ajax requests are sent from there and corresponding actions are dispatched with the response payload. I think it makes it easier to understand. (at least for my brain). The only "problem" is to structure the code correctly to not end with 100's of actions in the same place.Alyse

© 2022 - 2024 — McMap. All rights reserved.