Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vue3 reactivity of an "external" array

Tags:

vue.js

vuejs3

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
like image 292
Alex from Jitbit Avatar asked Oct 16 '25 09:10

Alex from Jitbit


1 Answers

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()
like image 119
Dimava Avatar answered Oct 18 '25 13:10

Dimava



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!