#5629·vueuse

useVModel({ passive: true }) silently drops a prop change that arrives while isUpdating is set

Author: ismoil77Created Sep 9, 2026Updated Sep 9, 2026

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