Subscribe on changes!

watchEffect tracks additional dependencies causing a render loop

avatar
Feb 4th 2022

Version

3.2.29

Reproduction link

stackblitz.com

Steps to reproduce

  1. open the link to minimal reproduction.
  2. open dev tools
  3. wait for the server to start.
  4. notice how multiple refs are tracked on top of props.msg.

What is expected?

Only props.msg should be tracked.

What is actually happening?

Refs inside vuex's mutation are getting tracked by watchEffect, which will cause the effect to trigger if any data in the store changes even though it isn't used by the watch itself. This will cause all sensible uses of vuex mutations (ones that change the state) in a watchEffect to trigger in loops.


Notes

I'm not too sure of what to expect here. To me, it's obvious that you should be able to dispatch/commit inside a watcher, but I also understand that you might want to call a local function with tracked refs as well. I think the solution should take both situations into account.

Workaround

It is possible to get a correct behavior by using watch with the { immediate: true } option, but this solution forces you to specify all of the watched refs twice which might lead to subtle bugs if they are not properly updated.

avatar
Feb 4th 2022

watchEffect tracks all reactive property access that happens during synchronous execution. So this is expected and can't be prevented automatically for the reasons you laid out yourself.

You can regulate what gets tracked by pushing something on a next Tick, for example:

watchEffect(async ()=> {
const msg = props.msg // sync access, tracked
await nextTick() // now we go async
store.commit('updateBiz', msg) // untracked
})

Also, it might be worth exploring wether Vuex should simply be pausing tracking altogether during mutations. That would have to be discussed in the vuex repo, though.

Also, I'm not getting your point about watch() here:

but this solution forces you to specify all of the watched refs twice which might lead to subtle bugs if they are not properly updated.

How do refs have to be specified twice?

avatar
Feb 4th 2022

Thanks for the nextTick() trick. I'll remember that.

it might be worth exploring wether Vuex should simply be pausing tracking altogether during mutations.

Do you want me to open an issue over in the vuex repo to discuss this? I think it makes a lot of sense, and is also what I expected initially.

How do refs have to be specified twice?

Assuming that you want the same behavior to watchEffect which would track every refs automatically, it means that every ref you are using in your callback also have to be specified in the source. The equivalent call for you example would be this:

watch(
  () => props.msg,
  () => store.commit('updateBiz', props.msg),
  { immediate: true },
)

I think you can see how a larger callback, with multiple refs, would lead to more risk of mismatch with the source parameter. For smaller callbacks with a single ref, it's just more verbose so it's not too much of an issue. It is also totally fine if you're looking for a different behavior.

avatar
Feb 4th 2022

Do you want me to open an issue over in the vuex repo to discuss this? I think it makes a lot of sense, and is also what I expected initially.

Yes. Could be an actual bug in Vuex.

avatar
Feb 4th 2022

I'll close this here as the behavior is fine as far as Vue core is concerned.