Vapor interop: a Vapor child's uncontrolled `defineModel` resets when its VDOM parent re-renders
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:
- VDOM only:
createApp(PlainParent) - Vapor only:
createVaporApp(VaporParent) - Interop:
createApp(InteropParent).use(vaporInteropPlugin), a VDOM parent rendering the Vapor SFCChild - Interop, same as 3 but
Childis written by hand withdefineVaporComponent(src/HandChild.ts)
Child owns a model its parent never binds:
const text = defineModel<string>('text', { default: '' })
defineProps<{ n: number }>()The parent only passes an unrelated prop:
<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
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.9Any 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:
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.
Source: vuejs/core