I'm working with some objects (classes) in my TS codebase which perform async operations right after their creation. While everything is working perfectly fine with Vue 2.x (code sample), reactivity breaks with Vue3 (sample) without any errors. The examples are written in JS for the sake of simplicity, but behave the same as my real project in TS.
import { reactive } from "vue";
class AsyncData {
static Create(promise) {
const instance = new AsyncData(promise, false);
instance.awaitPromise();
return instance;
}
constructor(promise, immediate = true) {
// working, but I'd like to avoid using this
// in plain TS/JS object
// this.state = reactive({
// result: null,
// loading: true,
// });
this.result = null;
this.loading = true;
this.promise = promise;
if (immediate) {
this.awaitPromise();
}
}
async awaitPromise() {
const result = await this.promise;
this.result = result;
this.loading = false;
// this.state.loading = false;
// this.state.result = result;
}
}
const loadStuff = async () => {
return new Promise((resolve) => {
setTimeout(() => resolve("stuff"), 2000);
});
};
export default {
name: "App",
data: () => ({
asyncData: null,
}),
created() {
// awaiting promise right in constructor --- not working
this.asyncData = new AsyncData(loadStuff());
// awaiting promise in factory function
// after instance creation -- not working
// this.asyncData = AsyncData.Create(loadStuff());
// calling await in component -- working
// this.asyncData = new AsyncData(loadStuff(), false);
// this.asyncData.awaitPromise();
},
methods: {
setAsyncDataResult() {
this.asyncData.loading = false;
this.asyncData.result = "Manual data";
},
},
};
<div id="app">
<h3>With async data</h3>
<button @click="setAsyncDataResult">Set result manually</button>
<div>
<template v-if="asyncData.loading">Loading...</template>
<template v-else>{{ asyncData.result }}</template>
</div>
</div>
The interesting part is, that the reactivity of the object seems to be completely lost if an async operation is called during its creation.
My samples include:
- A simple class, performing an async operation in the constructor or in a factory function on creation.
- A Vue app, which should display "Loading..." while the operation is pending, and the result of the operation once it's finished.
- A button to set the loading flag to false, and the result to a static value manually
- parts commented out to present the other approaches
Observations:
- If the promise is awaited in the class itself (constructor or factory function), the reactivity of the instance breaks completely, even if you're setting the data manually (by using the button)
- The call to awaitPromise happens in the Vue component everything is fine.
An alternative solution I'd like to avoid: If the state of the AsyncData (loading, result) is wrapped in reactive() everything works fine with all 3 approaches, but I'd prefer to avoid mixing Vue's reactivity into plain objects outside of the view layer of the app.
Please let me know your ideas/explanations, I'm really eager to find out what's going on :)
EDIT: I created another reproduction link, which the same issue, but with a minimal setup: here