#15670·Vue.js

Vapor interop: a Vapor child's uncontrolled `defineModel` resets when its VDOM parent re-renders

Author: Mini-SylarCreated Sep 27, 2026Updated Sep 27, 2026

Vue version

3.6.0-rc.9

Link to minimal reproduction

https://stackblitz.com/~/github.com/Mini-Sylar/vapor-interop-model-reset-repro

Steps to reproduce

The reproduction mounts the same Parent/Child pair four ways:

  1. VDOM only: createApp(PlainParent)
  2. Vapor only: createVaporApp(VaporParent)
  3. Interop: createApp(InteropParent).use(vaporInteropPlugin), a VDOM parent rendering the Vapor SFC Child
  4. Interop, same as 3 but Child is written by hand with defineVaporComponent (src/HandChild.ts)

Child owns a model its parent never binds:

typescript
const text = defineModel<string>('text', { default: '' })
defineProps<{ n: number }>()

The parent only passes an unrelated prop:

xml
<Child :n="n" />
<button @click="n++">2. Re-render the parent (n++)</button>

In each column, click "1. Set text from inside the child", then "2. Re-render the parent (n++)".

What is expected?

text stays "set by child" in every column. The parent never binds v-model:text, so re-rendering it should not touch the child's model.

What is actually happening?

Mount After 1 After 2
VDOM only "set by child" "set by child"
Vapor only "set by child" "set by child"
Interop "set by child" ""
Interop, hand-written child "set by child" ""

In both interop columns the model is reset to its default on any parent re-render. Without a default it resets to undefined. Same result in dev and in a production build.

System Info

bash
System:
  OS: macOS 27.0
  CPU: (10) arm64 Apple M5
  Memory: 76.63 MB / 16.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  pnpm: 12.5.1
Browsers:
  Safari: 27.0
npmPackages:
  vue: 3.6.0-rc.9 => 3.6.0-rc.9

Any additional comments?

Child is a Vapor component in both interop columns: Child.__vapor is true, and isVaporComponent(childVNode.component), read from the VDOM parent, is true. Both are printed on the page.

Found in vael-ui: its Vapor Combobox keeps the typed text in an uncontrolled defineModel('query'), so the input goes blank right after picking an option whenever the VDOM parent re-renders from its own v-model update.

Likely cause. Under interop, every parent update re-triggers effects that read the Vapor child's props, even when the prop's value didn't change. Counting runs of watchSyncEffect(() => props.text) in the child across 3 unrelated parent re-renders:

Mount Runs
VDOM only 1 → 1
Vapor only 1 → 1
Interop 1 → 4

useModel then resets the local value, because its sync effect compares the prop against localValue on every run rather than checking whether the prop itself changed:

javascript
watchSyncEffect(() => {
  const propValue = props[camelizedName]
  if (hasChanged(localValue, propValue)) {
    localValue = propValue
    trigger()
  }
})

Replacing defineModel with a text prop, an update:text emit and a local ref synced by watch(() => props.text, ...) does not reset, since watch skips the callback when the value is unchanged. A parent that binds v-model:text is also unaffected.