Search code examples
vue.jsvuejs3vue-reactivity

Vue3 Loosing reactivity on object, when being passed as prop


I've encountered a strange behavior while migrating from Vue2 to Vue3. When creating a new object in the parent component with an asynchronous function being called in the constructor, that manipulates the object, the child component, which this object is being passed, is not updated.

Here the code:

App.vue

<script>
import { ExampleObject } from './ExampleObject';
import Comp from './Comp.vue';
import Comp1 from './Comp1.vue';
export default {
  data() {
    return {
      someObjectsWithFetch: [],
      someObjectsWithoutFetch: [],
    }
  },

  components: {
    Comp,
    Comp1
  },

  mounted() {
    this.someObjectsWithFetch = [
      ExampleObject.createWithFetch(),
      ExampleObject.createWithFetch(),
    ];
    this.someObjectsWithoutFetch = [
      new ExampleObject(),
      new ExampleObject(),
    ];
  }
}
</script>

<template>
  <div class="container">
  <template v-if="someObjectsWithFetch.length > 0" v-for="object in someObjectsWithFetch">
    <Comp :some-object="object"></Comp>
  </template>
  <template v-if="someObjectsWithoutFetch.length > 0" v-for="object in someObjectsWithoutFetch">
    <Comp1 :some-object="object"></Comp1>
  </template>
  </div>
</template>

Comp.vue

<script>
export default {
  props: {
    someObject: {
      type: Object,
      required: true,
    }
  },

  computed: {
    isCompleted() {
      return this.someObject.status === 'completed';
    }
  }
}
</script>

<template>
  <span v-if="isCompleted">Completed</span>
  <span v-else>Pending</span>
</template>

Comp1.vue

<script>
export default {
  props: {
    someObject: {
      type: Object,
      required: true,
    }
  },

  created() {
    this.someObject.fetchStatus();
  },

  computed: {
    isCompleted() {
      return this.someObject.status === 'completed';
    }
  }
}
</script>

<template>
  <span v-if="isCompleted">Completed</span>
  <span v-else>Pending</span>
</template>

ExampleObject.js

export class ExampleObject {
  constructor() {
    this.status = 'pending';
  }

  static createWithFetch() {
    const newObject = new this;
    newObject.fetchStatus();
    return newObject;
  }

  async fetchStatus() {
    setTimeout(() => {
      this.status = 'completed';
    }, 3000);
  }
}

Working example

In Vue2 by calling the function in the constructor, the child component is actually updated when the property status has changed. In Vue3 it doesn't work.

To fix this, I passed the object to the child component Comp1 and called the asynchronous function in the created() hook. If this function is not being called in the component, but in the constructor of the object class, the reactivity seems to be lost.

Is there any other solution, than calling the function in the child component?


Solution

  • The reactivity of Vue 2 is based on modifying existing object by adding reactive getters and setters, while Vue 3 create Proxy wrappers around raw objects. There will be problems with reactivity as long as this refers raw object and not reactive proxy.

    This is the same problem. Classes should be restricted in a way they are designed in order to conform Vue reactivity.

    Asynchronous side effects on construction have code smell, this doesn't allow to control them during the lifespan of an instance. In case of async function this discards a promise.

    A quick fix would be to make the class use Vue reactivity:

    const newObject = reactive(new this);
    

    A cleaner way to do this would be to decouple construction and asynchronous side effects:

    this.someObjectsWithFetch = [
      new ExampleObject(),
      new ExampleObject(),
    ];
    
    for (let obj of this.someObjectsWithFetch)
      // obj is reactive
      obj.fetchStatus()
    

    Where the explicit call of fetchStatus allows to chain it or provide the cleanup logic. In this case it would involve clearTimeout on destroy, not doing this can result in memory leaks and related warnings in a console.