Search code examples
javascriptvue.jsvuejs3es6-proxy

Class Reactivity with Proxy does not work as expected in Vue 3


I have a Class with a proxy-based object, the set() method changes another property of the same class, everything works fine if I run the code only in JS/TS.

class Form {
  errors = []
  values = {}

  constructor(values) {
    this.values = new Proxy(values, {
      set: (target, prop, value) => {
        target[prop] = value

        this.errors.push('test')

        return true
      },
    })
  }
}

const form = new Form({
  name: 'Jhon',
  age: 20,
})

form.values.name = 'Maria'
form.values.age = 30

console.log(form.errors)

Expected result of form.errors is an Array like ['test', 'test']

But if I run it inside Vue, using {{ form.errors }} inside <template> it's not reactive, it's not updated in real time.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@3"></script>
  </head>
  <body>
    <script src="https://unpkg.com/vue@3"></script>

    <div id="app">{{ form.errors }}</div>

    <script>
      class Form {
        errors = []
        values = {}

        constructor(values) {
          this.values = new Proxy(values, {
            set: (target, prop, value) => {
              target[prop] = value

              this.errors.push('test')

              return true
            },
          })
        }
      }

      const app = Vue.createApp({
        data() {
          return {
            form: new Form({
              name: 'Jhon',
              age: 20,
            }),
          }
        },
        mounted() {
          this.form.values.name = 'Maria'
          this.form.values.age = 30
        },
      })

      app.mount('#app')
    </script>
  </body>
</html>

form.errors is updated, but this does not reflect in Vue, it is as if Vue cannot observe these changes, the proof of this is that if we do

mounted() {
  this.form.values.name = 'Maria'
  this.form.values.age = 30

  this.form.errors.push('hello')
}

we will have the expected result in the DOM

['test', 'test', 'hello']

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@3"></script>
  </head>
  <body>
    <script src="https://unpkg.com/vue@3"></script>

    <div id="app">{{ form.errors }}</div>

    <script>
      class Form {
        errors = []
        values = {}

        constructor(values) {
          this.values = new Proxy(values, {
            set: (target, prop, value) => {
              target[prop] = value

              this.errors.push('test')

              return true
            },
          })
        }
      }

      const app = Vue.createApp({
        data() {
          return {
            form: new Form({
              name: 'Jhon',
              age: 20,
            }),
          }
        },
        mounted() {
          this.form.values.name = 'Maria'
          this.form.values.age = 30

          this.form.errors.push('okay')
        },
      })

      app.mount('#app')
    </script>
  </body>
</html>

What I want is for form.errors to be reactive in Vue just like any other property.


Solution

  • To make Form#errors reactive in this case, initialize it with Vue.reactive():

    class Form {
      errors = Vue.reactive([])
      ⋮
    }
    

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <script src="https://unpkg.com/vue@3"></script>
      </head>
      <body>
        <div id="app">form.errors: {{ form.errors }}</div>
    
        <script>
          class Form {
            errors = Vue.reactive([])
            values = {}
    
            constructor(values) {
              this.values = new Proxy(values, {
                set: (target, prop, value) => {
                  target[prop] = value
    
                  this.errors.push('test')
    
                  return true
                },
              })
            }
          }
    
          const app = Vue.createApp({
            data() {
              return {
                form: new Form({
                  name: 'Jhon',
                  age: 20,
                }),
              }
            },
            mounted() {
              this.form.values.name = 'Maria'
              this.form.values.age = 30
              
              this.form.errors.push('hello')
            },
          })
    
          app.mount('#app')
        </script>
      </body>
    </html>