Vuex: Calls to mutations and actions don't get cancelled when a module is unregistered using `unregisterModule`

Created on 3 Jun 2020  路  8Comments  路  Source: vuejs/vuex

Version

3.4.0

Reproduction link

https://codesandbox.io/s/mutation-call-after-unregistermodule-v4n3z

Steps to reproduce

  1. Make sure the first page you see is the Home page.
  2. Click on Contact button and immediately click on Home
  3. Observe the Console.

What is expected?

No errors. When the module is unregistered, any running action or mutation calls should also be cancelled.

What is actually happening?

The action/mutation code keeps executing and user sees errors in the Console stating that the module does not exist.
[vuex] unknown local mutation type: setContact, global type: contact/setContact


There are 2 components: Home.vue and Contact.vue. Contact.vue calls an action in it's mounted callback. The action changeContact mimics the behaviour of an API call and commits a mutation after a delay of 1 second.

What I am trying to do is that when user navigates from one page to another, the modules of the first route should be unregistered. This can be done easily using the unregisterModule method. The problem is that any action or mutation calls executed before the unregistering are not removed and they throw errors in the browser Console.

In the above sandbox, when user switches from Contact to Home immediately, following things happen:

  • Contact.vue loads and it's mounted callback is called.
  • Inside the mounted callback, the changeContact action is called
  • User goes back to Home. This calls beforeDestroy of Contact.vue that un-registers the contact module.
  • The changeContact tries to call the setContact mutation, but, since the module does not exist, an error message is thrown in the Console [vuex] unknown local mutation type: setContact, global type: contact/setContact
discussion enhancement

All 8 comments

OK, this is pretty interesting. Yea, this is expected at the moment since Vuex wouldn't do anything about the cancelation of the logic execution.

However, I think this is very hard to implement in Vuex. While actions could return Promise, I don't think we can "cancel" the promise execution (in terms of JS spec).

I think users have to deal with such a situation on their own. For example, in your example, you should return setTimeout and cancel it before you call unregisterModule. If it's XHR request, you should use the cancel feature of XHR request lib.

I'll keep this issue open to gather some feedback if anybody knows a way to cancel the promise execution in JS. It has to be configurable though, since there might be the case when you want Promises to be executed, even the module is unregistered.

@kiaking Thanks for replying.

For example, in your example, you should return setTimeout and cancel it before you call unregisterModule. If it's XHR request, you should use the cancel feature of XHR request lib.

The setTimeout was used in the example to just mimic the XHR behaviour. I understand setTimeout and XHR requests can be cancelled. But, I am encountering scenarios where user has routed to another page, the XHR requests have been cancelled, but, the calls to mutations (context.commit) have still thrown errors.

I'll elaborate some more on the problem I have faced:

Imagine having actions that have the following logic:

  • first commit a mutation to set loading state to true. This help to show a progress bar in the app and shows that XHR call is in-progress.

    • Make the XHR request

    • Once the request is complete, mark loading state to false.

I am encountering a flow of this kind:

- User visits a page 
- The different actions for the page are called.
- User routes back 
- Code to un-register modules get executed. First all the API calls are cancelled and then, `unregisterModule` is called.

Now, in the above case, all the in-progress XHR requests get cancelled, but, there are some actions that have just started executing when the modules get unregisted. These actions haven't started any XHR requests. They are just about to call context.commit to set loading to true. But, they are unable to do so because the module is un-registered. Hence the actions end up throwing the error [vuex] unknown local mutation type

So, yeah, as you said, we need to capture the Promise returned by an action and then, cancel it by some means. This way, hopefully, the execution of context.commit would be prevented.

So, yeah, as you said, we need to capture the Promise returned by an action and then, cancel it by some means. This way, hopefully, the execution of context.commit would be prevented.

Yeah this is the hard part I guess. One thing I could think is that can't you wait until Promise execution is done before calling unregisterModule?

const promise = store.dispatch('api/longRunningRequest')

promise.then(() => {
  store.unregisterModule('api')
})

So, yeah, as you said, we need to capture the Promise returned by an action and then, cancel it by some means. This way, hopefully, the execution of context.commit would be prevented.

Yeah this is the hard part I guess. One thing I could think is that can't you wait until Promise execution is done before calling unregisterModule?

const promise = store.dispatch('api/longRunningRequest')

promise.then(() => {
  store.unregisterModule('api')
})

The purpose here is that when user goes from one page to another, the API calls of the previous page should be cancelled and the Vuex modules should be un-registered. So, if we wait for the promises to resolve, it defeats the whole purpose.
Plus in large apps, we really don't wait for promises to resolve. We call an action and expect the mutation to set data in the store once the XHR is complete. Inside our component, a computed property gets data from the store. So, once the API call completes, the data just shows up in the component. We don't keep a list of running Promises in the memory.

Yes I mean I thought you could cancel the XHR, but just let whole other things execute till the end (committing and such).

I mean I think you could add beforeRouteLeave middleware to handle any "cleanup". So before a user navigate to another page;

  1. Cancel XHR request.
  2. Wait until all other action execution completes.
  3. Unregister Module.

This can be done "after" user have navigated to another page and it can be asynchronous. The request this issue looking for is to do this automatically right? I strongly think it's nearly impossible due to Vuex would have no idea how to properly cancel some action dispatches. Unless we use some custom Promise like lib.

The request this issue looking for is to do this automatically right?

Yes, the expectation was that Vuex will cancel action dispatches. XHR requests will obviously have to be cancelled by developers of the app.

I strongly think it's nearly impossible due to Vuex would have no idea how to properly cancel some action dispatches. Unless we use some custom Promise like lib.

I think you understand the problem and my use case well enough. The ball is in your court and I'll leave it up to you guys to decide whether a solution is to be explored or not.

Hi, we've discussed internally as well and came to a conclusion that we'll drop this feature request. First of all, there're no definitive ways to cancel the Promise execution in JS, and even if there were, we think it's almost impossible to handle correct canceling at the Vuex side.

These situation needs to be handled by users, since it's too specific to what app their building. For example, in the case you unregister a module and reregister it before the promise resolves, do you still want to commit that result to the store...? Handling all of this edge case would be really really hard in my opinion.

But thank you for bringing this up. Honestly I wasn't aware of this situation so hopefully we could do better in next Vuex iteration.

Thank you for having a healthy discussion on this. Please keep up the good work you guys have been doing. Cheers!

Was this page helpful?
0 / 5 - 0 ratings