useVModel({ passive: true }) silently drops a prop change that arrives while isUpdating is set
Describe the bug
With passive: true, useVModel protects itself from the emit → prop → proxy feedback loop with a boolean isUpdating that is cleared on nextTick. The guard is time based, so a second change of the prop that lands in the same tick — for example from a watch in the parent that runs after the composable's own watcher — is dropped silently: the proxy keeps the first value while the prop already holds the second one, nothing is emitted and there is no warning.
// packages/core/useVModel/index.ts
watch(() => props[key!], (v) => {
if (!isUpdating) { // <- a change arriving while the flag is set is ignored
isUpdating = true
proxy.value = cloneFn(v)
nextTick(() => isUpdating = false)
}
})
In practice it shows up as an input that displays a stale value while the form state is correct — whenever a parent computes the model through a chain of watchers, or rejects/adjusts a value right after receiving it. We hit it in three unrelated components before tracking it down to this.
Reproduction
No component needed:
import { reactive, watch, nextTick } from 'vue'
import { useVModel } from '@vueuse/core'
const props = reactive({ modelValue: 'a' })
const emitted: unknown[] = []
const proxy = useVModel(props, 'modelValue', (_e, v) => emitted.push(v), { passive: true })
// created after useVModel, so it runs after its watcher in the same flush
watch(() => props.modelValue, (v) => { if (v === 'b') props.modelValue = 'c' })
props.modelValue = 'b'
await nextTick()
await nextTick()
console.log(props.modelValue) // "c"
console.log(proxy.value) // "b" <- expected "c"
console.log(emitted) // []
Expected: the proxy follows the prop and ends up at "c".
Actual: props.modelValue === "c", proxy.value === "b", nothing emitted. The two stay out of sync until the prop changes again.
The flag was introduced in #3097 to stop the infinite loop with clone: true, so a fix probably has to compare values (or remember the last value pushed into the proxy) instead of relying on the tick boundary. Happy to send a PR if that direction sounds right.
System Info
@vueuse/core: 14.4.0 (the code is unchanged in main)
vue: 3.5.38
node: 24.16.0
Used Package Manager
bun
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Make sure this is a VueUse issue and not a framework-specific issue.
- Check that this is a concrete bug.
- The provided reproduction is a minimal reproducible example of the bug.
Source: vueuse/vueuse