I just came across the same problem. Let me share with you my understanding.
First let's establish a few facts.
As it's widely known v-model
is basically this:
<Component :modelValue="myObject" @update:modelValue="(newObject) => myObject = newObject/>
If you use the script setup, the const declared and other declarations become a property of an object.
So this:
const myObject = reactive({
id: 'App',
});
Becomes this:
export default {
setup() {
const myObject = reactive({
id: 'App',
});
return {
myObject,
};
},
}
Now let's put this knowledge together. When child component emits the event update:modelValue
with new object, what Vue does is an assignment to the object returned from setup. In pseudo code: setupObject.myObject = newObjectFromEvent
You can prove it by this simple code:
<script>
import { isProxy, reactive } from 'vue';
import Test from './Test';
export default {
components: {
Test
},
setup() {
const myObject = reactive({
id: 'App',
});
const setupObject = {
myObject,
};
setTimeout(() => {
console.log(setupObject.myObject);
console.log(isProxy(setupObject.myObject));
}, 5000);
return setupObject;
},
}
</script>
<template>
<Test v-model="myObject"/>
</template>
<script setup>
import { defineProps, isRef, watch } from 'vue';
const props = defineProps(['modelValue']);
watch(props.modelValue, () => {
console.log('Value changed!');
});
</script>
<template>
<button @click="$emit('update:modelValue', { id: 'Test' })">Click me</button>
</template>
What you will observe after clicking the button is that our console.log
will output: {id: 'Test'}
and false
.
Also the watcher from the Test component wouldn't be triggered, cause our property is no longer reactive and our Test component still holds the old value of myObject.
The exact same thing happens with the draggable v-model. When you drag and drop the item, the property (in your case myArray
) that becomes part of a larger object created upon completing the setup code is overridden with the non-reactive object emitted from draggable component, thus you don't see item position changed.
Why it works with ref you might ask? Well, going back to our @update:modelValue="(newObject) => myObject = newObject
if you use ref, the Proxy doesn't vanish, because the assignment is done to the myObject.value
property. Thus when you used ref it has started working.
Other way to make it work with reactive
is to handle the update manually:
const handleUpdate = (newObject) => {
Object.assign(myObject, newObject);
};
<Test :modelValue="myObject" @update:modelValue="handleUpdate"/>
This way you wouldn't loose the reactivity, cause you keep the same Proxy and just reassign properties.
The bottom line is you have to be very cautious when using v-model with reactive objects or arrays.