martyjs / marty

A Javascript library for state management in React applications
http://martyjs.org
MIT License
1.09k stars 76 forks source link

Cannot dispatch in the middle of a dispatch #151

Closed FoxxMD closed 9 years ago

FoxxMD commented 9 years ago

I am receiving the error Uncaught Error: Invariant Violation: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. when trying to perform an action but under almost identical conditions performing another action never receive this. I'm hoping someone can shed some light on what I'm missing...

I will describe both actions in parallel.

Action creator is called

new.jsx

component = React.createClass({
    displayName:'New Merchant',
    mixins: [Router.Navigation, MerchantStateMixin],
    handleIt: function (data) {
        this.setState({
            active: true
        });
        MerchantActionCreator.addMerchant(data);
    },

details.jsx

var merchantDetails = React.createClass({
    displayName: 'Edit Merchant',
    mixins:[Router.State, Router.Navigation, MerchantStateMixin],
    handleEdit: function (data) {
        var that = this;
        this.setState({
            active: true
        });
        MerchantActionCreator.updateMerchant(data);
    },

Action creator calls source and clears 'status' state in store

var actionCreators = Marty.createActionCreators({
    updateMerchant: constants.UPDATE_MERCHANT(function (merchant) {
        this.clearStatus();
        return merchantSource.updateMerchant(merchant);
    }),
    addMerchant: constants.ADD_MERCHANT(function (merchant) {
        this.clearStatus();
        return merchantSource.addMerchant(merchant);
    }),
    updateStatus: constants.UPDATE_STATUS(),
    clearStatus: constants.CLEAR_STATUS()
});

Source calls api and then calls source action creator

    addMerchant: function (merchant) {
        Api.post('merchant_save', merchant.toApi()).then(function (res) {
            merchant.apiKey = res.data.m_api_key;
            merchant.id = res.data.m_id;
            MerchantSourceActionCreators.addMerchant(merchant);
        }).catch(function (error) {
            console.log(error);
            MerchantSourceActionCreators.updateStatus('failed');
        })
    },
    updateMerchant: function (merchant) {
        Api.post('merchant_save', merchant.toApi()).then(function (res) {
            MerchantSourceActionCreators.updateMerchant(merchant);
        }).catch(function(error){
            console.log(error);
            MerchantSourceActionCreators.updateStatus('failed');
        })
    }

Source action creator dispatches to store

    addMerchant: constants.ADD_MERCHANT(function(payload){
        if(payload instanceof Models.Merchant){
            this.dispatch(payload);
        }
        else{
            var merch = new Models.Merchant();
            merch.fromApi(payload);
            this.dispatch(merch);
        }
    }),
    updateMerchant: constants.UPDATE_MERCHANT(function(payload){
        if(payload instanceof Models.Merchant){
            this.dispatch(payload);
        }
        else{
            var merch = new Models.Merchant();
            merch.fromApi(payload);
            this.dispatch(merch);
        }
    }),
    updateStatus: constants.UPDATE_STATUS()

Store updates state and notifies of changes, then calls to update status

    updateMerchant: function (merchant) {
        this.state[merchant.id] = merchant;
        this.hasChanged();
        this.updateActionStatus('success');
    },
    addMerchant: function (merchant) {
        this.state[merchant.id] = merchant;
        this.hasChanged();
        this.updateActionStatus('success');
    },
    updateActionStatus: function (status) {
        this.state['status'] = status;
        this.hasChanged();
    },
    clearActionStatus: function () {
        this.state['status'] = null;
    },

Finally, components update and another action creator is invoked

new.jsx

    componentDidUpdate: function(){
        var that = this;
        if(this.state.active && this.getState().status != null){
            this.setState({
                active: false
            });
            if(this.getState().status == 'success'){
                /*
                * This is where the dispatcher error occurs. Removing NotifyAction makes the error stop
                * 
                * */
                NotifyAction.addNotification('Merchant Added', undefined, undefined, 'success');
                that.transitionTo('Merchants');
            }
        }
    }

details.jsx

    componentDidUpdate: function(){
        if(this.state.active && this.getState().status != null){
            this.setState({
                active: false
            });
            if(this.getState().status == 'success')
                NotifyAction.addNotification('Merchant Updated', undefined, undefined, 'success');
        }
    }

As you can see the flow for both of these use cases are very similar. This is because right now they basically do the same thing, however I plan on adding more functionality to both as I develop the app. But when new.jsx does its thing I get the error -- and never receive it when using detail.jsx.

What am I missing??

jhollingworth commented 9 years ago

Difficult to tell from your code what the issue is but I'd recommend against invoking actions as a result of another action. A different approach which is a bit more in keeping with the flux architecture would be to have the store that listens to the ADD_NOTIFICATION action to instead listen to ADD_MERCHANT and UPDATE_MERCHANT but use waitFor to wait for the merchant store to update before querying it.

var NotificationsStore = Marty.createStore({
  handlers: {
    addMerchantAddedNotification: MerchantConstants.ADD_MERCHANT,
    addMerchantUpdatedNotification: MerchantConstants.UPDATE_MERCHANT
  },
  addMerchantAddedNotification: function () {
    this.waitFor(MerchantStore);

    var status = MerchantStore.getState().status;

    if (status === 'SUCCESS') {
      ...
    }
  },
  addMerchantUpdatedNotification: function () {
    this.waitFor(MerchantStore);

    var status = MerchantStore.getState().status;

    if (status === 'SUCCESS') {
      ...
    }
  }
})