While porting an existing app from Vue2 to Vue3 I faced a surprising problem.
How can I make Vue3 watch an "external" array for changes?
This worked perfectly fine in Vue2, but stopped working in Vue3:
<ul id="list">
<li v-for="t in dataArray"> {{t}} </li>
</ul>
<script>
var numbers = [1,2,3]; //this is my external array
var app = Vue.createApp({
data() { return { dataArray : numbers } } //bind Vue to my external array
}).mount("#list");
numbers.push(4); //UI not updating, but worked fine in Vue2
</script>
I know I can call app.dataArray.push
instead, or call $forceUpdate
, etc. but is there a way to force Vue to simply monitor an existing array?
I guess the broader question is: how to bind Vue3 to an arbitrary plain-JS object? The object can be too complex to rewrite or can come from an external API that I don't control. This is trivial in Vue2 or Angular (two-way binding with any plain object, whether or not it's part of the instance/component)
P.S. This looks like a huge breaking change in Vue3 that is not mentioned anywhere at all.
UPDATE:
According to a @Dimava's answer it looks looks like the least painful way of fixing the above code is this:
var numbers = [1,2,3]; //my external array that came from API
numbers = Vue.shallowReactive(numbers); //convert to a reactive proxy
You need to make your array Reactive
¹
import { reactive, ref } from 'vue'
const numbers = [1,2,3];
const reactiveNumbers = reactive(numbers)
reactiveNumbers.push(4)
// or, if you will need to reassign the whole array
const numbersRef = ref(numbers)
numbersRef.value.push(4)
numbersRef.value = [3, 2, 1]
// or, in the old style, if you are old
const data = reactive({
numbers: [1, 2, 3]
})
data.numbers.push(4)
data.numbers = [3, 2, 1]
¹ (or ShallowReactive
if it contains a lot of big objects that shouldn't be reactive for performance reasons)
Edit: you can do it with Proxy, but it won't wort for get/set on numbers It will work for objects tho
import { reactive, watch } from 'vue'
function reactify(obj) {
if (Array.isArray(obj)) {
const clone = reactive([...obj])
Object.setPrototypeOf(obj, new Proxy(clone, {
get(target, prop) {
console.log(' get', prop, target[prop])
if (typeof prop == 'number')
return target[prop]
const v = target[prop]
if (typeof v == 'function')
return v.bind(target)
return v
},
set(target, prop, value) {
console.log(' set', prop, value)
target[prop] = value
return true
},
}))
return clone
}
else {
const clone = reactive({ ...obj })
Object.setPrototypeOf(obj, new Proxy(clone, {
get(target, prop) {
return target[prop]
},
set(target, prop, value) {
target[prop] = value
return true
},
}))
return clone
}
}
const externalArray = [123, 'string', true, [1, 2, 3], { a: 1, b: 2 }]
const reactiveArray = reactify(externalArray)
watch(reactiveArray, () => {
console.log('reactiveArray changed')
}, { deep: true })
const sleep = () => new Promise(setImmediate)
console.log('push')
externalArray.push(456)
await sleep()
console.log('pop')
externalArray.pop()
await sleep()
console.log('set')
externalArray[0] = 123 // DOESN'T LOG
await sleep()
console.log('splice')
externalArray.splice(0, 1, 456)
await sleep()
console.log('deep set')
externalArray[4].a = 123 // DOESN'T LOG
await sleep()