This is basic stuff, I’m missing something obvious.
I have reproduced the basic v-model example from the docs:
https://vuejs.org/guide/components/v-model.html#basic-usage
and it is working fine. I have added the: @update:modelValue="modelUpdated"
handler and it works: the child component detects a change and emits the update event and everything is fine:
Vue.js Playground
BUT, when I change the example from the primitive value (number) to object the event is no longer emitted!
So, instead of:
const countModel = ref(0)
now I’m using:
const countModel = ref({
name: "Joe",
age: 30
})
The object IS changed, but I’m not being notified. Tried watching that object too, did not work:
Vue.js Playground
How do I listen (from the parent) for the changes that the child component did via v-model
?
0
The ref
means it’s a reference and to track/watch it, it should be changed, not the object it reference to:
Playground
function update() {
model.value = {...model.value, age: model.value.age + 1};
}
I actually consider this as a weak part of Vue, since creating a big object each time its property changes could be slow, non meaning that any references to nested object are destroyed.
On the other hand you can just watch the object:
watch(countModel.value, modelUpdated);
But that doesn’t imply the modelUpdate event so v-model
is useless and you’d better use an usual prop but Vue considers props readonly but doesn’t protect object props from mutating since “it’s expensive”.
So I would say the whole 2-way binding of objects looks not well designed and consistent in Vue.
But in general I’d say v-model is used for promitive types and simple objects without nesting, if I want to pass a complex object I’d rather use provide/inject or an import that exported into child components
3
As @AlexanderNenashev also noted, defineModel
is best suited for simpler data types. In my second example, you’ll see that defineModel
practically loses its purpose since I activate a new watcher.
Vue’s v-model
by default (with deep: false
) cannot deeply observe the properties of an object (the Vue Docs mention, it is too expensive and not cost-effective). If you modify only one of the object’s internal properties (like age
), Vue does not automatically detect this change, which is why the update:modelValue
event does not get triggered.
You can, however, force the second case into a deep watcher by explicitly using the
deep
option.In Vue 3.5+, the
deep
option can also be a number indicating the max traversal depth – i.e. how many levels should Vue traverse an object’s nested properties.
Use with Caution
Deep watch requires traversing all nested properties in the watched object, and can be expensive when used on large data structures. Use it only when necessary and beware of the performance implications.
Source: Deep Watchers – Vue Docs
Solution # 1
Call manually update:modelValue
emit.
const model = defineModel()
const emit = defineEmits(['update:modelValue'])
function update() {
model.value.age++
emit('update:modelValue', model.value) // call manually
}
Vue Playgound
Solution # 2
Define own watch
with deep: true
property.
Note: In this case, I wouldn’t use defineModel
in this case anymore, as it would unnecessarily trigger another non-deep watcher.
import { watch } from 'vue'
const model = defineModel()
const emit = defineEmits(['update:modelValue'])
// Watch the model deeply
watch(
() => model.value,
(newValue) => {
emit('update:modelValue', newValue)
},
{ deep: true }, // This makes the watcher listen to nested changes
)
function update() {
model.value.age++
}
Vue Playgound
Solution # 3
You have the option to observe not only the object itself but also an internal property. This is more cost-effective than the previous deep observation, especially if, as in the example, only changes to age
are important to you.
import { watch } from 'vue'
const model = defineModel()
const emit = defineEmits(['update:modelValue'])
// Watch the model.age
watch(
() => model.value.age,
() => {
emit('update:modelValue', model.value);
},
)
function update() {
model.value.age++
}
Vue Playground