Subscribe on changes!

Computed values are not consistent in watch callback

avatar
Apr 14th 2022

Version

3.2.33

Reproduction link

sfc.vuejs.org/

The absolute minimal test case that prints to console:

import { computed, ref, watch } from 'vue';
const count = ref(0)
const plusOne = computed(() =>count.value + 1)

watch(count, () => {
  console.log('watch count:', count.value)
  console.log('watch plusOne:', plusOne.value)
})

console.log('before inc plusOne:', plusOne.value)
count.value++
console.log('after inc plusOne:', plusOne.value)

Steps to reproduce

Look at the printed output, notice the value printed with label "watch plusOne".

What is expected?

Computed values should recompute after its dependency is updated. That update should be immediately visible in the watch handler.

Expected output:

before inc plusOne: 1
watch count: 1
watch plusOne: 2
after inc plusOne: 2

What is actually happening?

Computed value is not reevaluated on read, even though the dependency have been changed since last read.

Actual output:

before inc plusOne: 1
watch count: 1
watch plusOne: 1
after inc plusOne: 2

This behaviour is a root cause for data inconsistencies in called code, as part of the data is current, and part is outdated. The watch callbacks are often used to perform external effects, and those effects end up operating on stale data.

The workaround in this specific case would be to watch the "plusOne" computed value instead. It becomes more complex when the refs and computed values are scattered across different functions or there are more complex computed value chains.

avatar
Apr 14th 2022

Additional examples

<script setup>
import { computed, ref, watch, nextTick } from 'vue';

const count = ref(0)
const plusOne = computed(() => {
  console.log(Object.assign({},count))
  console.log("computed",new Date().valueOf())
  return count.value + 1
})

const log = []

watch(count, (newVal,oldVal) => {
  console.log(newVal,oldVal)
  log.push(`watch count: ${count.value}`)
  // Here `computed` is not get the lastest value
  log.push(`watch plusOne: ${plusOne.value}`)
  console.log("watch",new Date().valueOf())
})

// Here `computed` is get the lastest value
log.push(`before inc plusOne: ${plusOne.value}`)
count.value++
// Here `computed` is get the lastest value
log.push(`after inc plusOne: ${plusOne.value}`)
</script>

<template>
  <h1 v-for="line in log">{{ line }}</h1>
</template>

image

avatar
Apr 14th 2022

This scenario should use effect instead. see sfc

avatar
Apr 14th 2022

This scenario should use effect instead

Why should it? There is nothing in computed values documentation (that i know of) that suggests the current behaviour is expected. That effect example happens to work, but it looks like doing manually what computed values were designed to do for you.

avatar
Apr 14th 2022

This only seems to happen when the change happens synchronously after the watcher has been defined. Doing the count increment in onMounted gets the desired result:

SFC Playground

Without claiming a full understanding, it seems any trigger of the watch effect before the next flush leads to a synchronous call of the watcher callback, which is undesirable and IMHO unintended, so seems like a bug.

#5721 Seems like another manifestation of the same problem.

avatar
Apr 15th 2022

This scenario should use effect instead

Why should it? There is nothing in computed values documentation (that i know of) that suggests the current behaviour is expected. That effect example happens to work, but it looks like doing manually what computed values were designed to do for you.

sorry, I misread. this should be a bug.

import { computed, ref, watch,onMounted } from 'vue';

const count = ref(0)
const plusOne = computed(() => count.value + 1)
+plusOne.value // add this line ,make sure computed collect dependencies first
  
const log = []

watch(count, () => {
  log.push(`watch count: ${count.value}`)
  log.push(`watch plusOne: ${plusOne.value}`)
})

log.push(`before inc plusOne: ${plusOne.value}`)
count.value++
log.push(`after inc plusOne: ${plusOne.value}`)