2

I have a class Thing, whose constructor starts an asynchronous fetch operation. When the fetch completes, the result is assigned to a field on the Thing object:

  class Thing {
    constructor() {
      this.image = null
      this.load()
    }

    async load() {
      const response = await fetch('https://placekitten.com/200/300')
      const blob = await response.blob()
      this.image = await createImageBitmap(blob)
    }
  }

I'm using thing.image in a Vue component. The problem is that Vue doesn't pick up on the change in image when the Promise resolves. I think I understand why this happens: in the constructor, this refers to the raw Thing, and not Vue's reactive proxy wrapper. So the assignment to this.image ends up bypassing the proxy.

It works if I move the load call out of the constructor, so that this inside the load function refers to the reactive proxy. But that makes my Thing class harder to use.

Is there a better way to handle this issue?

Minimal example (Vue playground link):

<script setup>
  import { reactive } from 'vue'

  class Thing {
    constructor() {
      this.image = null
      this.load() // This does not trigger reactivity.
    }

    async load() {
      const response = await fetch('https://placekitten.com/200/300')
      const blob = await response.blob()
      this.image = await createImageBitmap(blob)
    }
  }

  const thing = reactive(new Thing())
  // thing.load() // This triggers reactivity as expected.
</script>

<template>
  <p v-if="thing.image">
    Image size is {{thing.image.width}}×{{thing.image.height}}
  </p>
  <p v-if="!thing.image">
    Loading...
  </p>
</template>
Thomas
  • 174,939
  • 50
  • 355
  • 478
  • I think the Ref doesn't call the constructor it just take the values and bind them to new object so we have to call them manually [Ref source](https://github.com/vuejs/vue/blob/cdd2df6171963096abb94600987f1706e40d2ab6/src/core/observer/index.ts#L131) – raghava patnam Jul 14 '22 at 11:11
  • @raghavapatnam I am not actually using `ref` in my actual code, but probably `reactive` under the hood (the object is assigned to the component's `data`). I just edited the example; sorry for the confusion! – Thomas Jul 14 '22 at 11:13
  • Technically with JavaScript standard load function should be executed but with Vue reactive / ref - they seem to return only object – raghava patnam Jul 14 '22 at 11:23
  • It's generally an antipattern to do async side effects in a constructor that you can't control. Especially because it doesn't play well with classes that are unaware of Vue reactivity – Estus Flask Jul 14 '22 at 13:00
  • 1
    Possible duplicate of https://stackoverflow.com/questions/67894487/vue-3-reactivity-not-triggered-from-inside-a-class-instance – Estus Flask Jul 14 '22 at 13:02
  • Does this answer your question? [Vue 3 reactivity not triggered from inside a class instance](https://stackoverflow.com/questions/67894487/vue-3-reactivity-not-triggered-from-inside-a-class-instance) – Duannx Jul 15 '22 at 02:20

1 Answers1

1

define your class attribute as a ref

<script setup>
  import { reactive, ref } from 'vue'

  class Thing {
    constructor() {
      this.image = ref(null)
      this.load()
    }

    async load() {
      const response = await fetch('https://placekitten.com/200/300')
      const blob = await response.blob()
      this.image.value = await createImageBitmap(blob)
    }
  }

  const thing = reactive(new Thing())
  // thing.load() // This triggers reactivity as expected.
</script>

<template>
  <p v-if="thing.image">
    Image size is {{thing.image.width}}×{{thing.image.height}}
  </p>
  <p v-if="!thing.image">
    Loading...
  </p>
</template>
Florent Bouisset
  • 962
  • 1
  • 5
  • 11
  • Yep, that worked, thanks! Caveat: when going through the reactive proxy object, `this.image` is no longer a `Ref`, but instead resolves automatically to the actual value. Even inside methods on `Thing`! – Thomas Jul 14 '22 at 11:44
  • ref are automatically unwrapped when accessing from a reactive, object: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#ref-unwrapping-in-reactive-objects – Florent Bouisset Jul 14 '22 at 11:55
  • Yeah. This means that `this.load()` works when called from within the constructor, but `thing.load()` will no longer work, because `this.image.value` is undefined. Something to be aware of! – Thomas Jul 14 '22 at 11:57
  • 1
    Yes because the load function as been proxied, you can retrieve the original object and use the load method on it by using toRaw: `const thing = reactive(new Thing()) const rawThing = toRaw(thing) rawThing.load()` – Florent Bouisset Jul 14 '22 at 12:30
  • `rawThing.load()` is doable but smells fishy. This indicates the fundamental problem, this.load() shouldn't have been called inside the constructor in the first place, just because this makes class act weird, even with the explicit use of reactivity api – Estus Flask Jul 14 '22 at 13:07