I'm developing an app using Vuejs and Vuex.
I've got a Vuex module called settings_operations. This module has the following action:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}
Edit: the to() is https://github.com/scopsy/await-to-js
With the following mutations:
[CHANGE_PASSWORD_PROCESSING] (state, { id }) {
state.push({
id,
status: 'processing'
})
},
[CHANGE_PASSWORD_ERROR] (state, { id, error }) {
state.push({
id,
error,
status: 'error'
})
}
And then, in the component I want to use this state slice:
computed: {
...mapState({
settings_operations: state => state.settings_operations
})
},
watch: {
settings_operations: {
handler (newSettings, oldSettings) {
console.log(newSettings)
},
deep: false
}
}
The problem is that when the changePassword action results in an error, the watch doesn't stop in the PROCESSING step, it goes directly to the ERROR moment so the array will be filled with 2 objects. It literally jumps the "processing" watching step.
A funny thing that happens is that if I add a setTimeout just like this:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
setTimeout(async () => {
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}, 500)
},
It works! The watch stops two times: the first tick displaying the array with the processing object and the second tick displaying the array with 2 objects; the processing one and the error one.
What am I missing here?
Edit:
I reproduced the problem here: https://codesandbox.io/s/m40jz26npp
This was the response given in Vue forums by a core team member:
Watchers are not run every time the underlying data changes. They are only run once on the next Tick if their watched data changed at least once.
your rejected Promise in the try block is only a microtask, it doesn’t push execution to the next call stack (on which the watchers would be run), so the error handling happens before the watchers are run.
additionally, when you mutat an object or array, the newValue and oldValue in a deep watcher will be the same. See the docs:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the
same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
and as a final sidenote, I’ve never seen anyone use an aray as the root state of a module, I have no idea if that will work for vuex in all possible circumstances. I certainly would not recommend doing this.
Edit with a better and more complete answer from the same member:
Why watchers are asynchronous at all? Because in the vast majority of use cases, watchers only need to react to the last synchrnous change that was done. In most cases (in the context of a component), it would be couterproductive to to react to every change since you would re-trigger the same behaviour mutliple times even though in the end, only the last state is the important one.
In other words: Running a watcher on each change by default would probably lead to apps that burn a lot of CPU cycles doing useless work. So watchers are implemented with an asynchronous queue that is only flushed on nexTick. And we don’t allow duplicate watchers then because the older instance of a watcher would apply to data that doesn’t “exist” anymore in that state once the queue is flushed.
An important note would be that this only applies to synchronous changes or those done within a microtask, i.e. in an immediatly resolving or failing promise - it would, for example, not happen with an ajax request.
Why are they implemented in a way that they are still not run after a microtask (i.e. an immediatly resolved promise? That’s a bit more coplicated to explain and requires a bit of history.
Originally, in Vue 2.0, Vue.nextTick was implemented as a microtask itself, and the watcher queue is flushed on nextTick. That meant that back then, a watcher watching a piece of data that was changed two times, with a microtask (like a promise) in between, would indeed run two times.
Then, around 2.4 I think, we discovered a problem with this implementation and switched Vue.nextTick to a macroTask instead. under this behaviour, both data chhanged would happen on the current call stack’s microtaks queue, while the watcher queue would be flushed at th beginning of the next call stack, wich means it will only run once.
We found a couple of new problems with this implementation that are much more common than the original issue with microtasks, so we will likely switch back to the microtask implementation in 2.6. Ugly, but necessary.
So, this should do the trick for now:
await Vue.nextTick();