Problem#
When using asynchronous setup(), you have to use effects and lifecycle hooks before the first await statement. (details)
import { ref, watch, onMounted, onUnmounted } from 'vue'
export default defineAsyncComponent({
async setup() {
const counter = ref(0)
watch(counter, () => console.log(counter.value))
// OK!
onMounted(() => console.log('Mounted'))
// the await statement
await someAsyncFunction() // <-----------
// does NOT work!
onUnmounted(() => console.log('Unmounted'))
// still works, but does not auto-dispose
// after the component is destroyed (memory leak!)
watch(counter, () => console.log(counter.value * 2))
}
})
After the await statement, the following functions will be limited (no auto-dispose):
watch
/watchEffect
computed
effect
The following functions will not work:
onMounted
/onUnmounted
/onXXX
provide
/inject
getCurrentInstance
Limitation#
Asynchronous functions can cause issues when updating global variables that are shared across multiple components. When setup()
is awaited, multiple components creation can change the global variable, making it unpredictable and creating a mess in the system. If setup()
is called without an await statement, it will execute tasks before the first await, affecting the consistency of the execution. Vue cannot predict when the asynchronous part will be called from the outside and therefore cannot bind the instance to the context.
currentInstance = instance
await component.setup() // atomic lost
currentInstance = prev
async function setup() {
console.log(1)
await someAsyncFunction()
console.log(2)
}
console.log(3)
setup()
console.log(4)
// output:
3
1
4
(awaiting)
2
Solutions#
Wrap the Async Function as "Reactive Sync"#
From
const data = await fetch('https://api.github.com/').then(r => r.json())
const user = data.user
To
const data = ref(null)
fetch('https://api.github.com/')
.then(r => r.json())
.then(res => data.value = res)
const user = computed(() => data?.user)
Explicitly Bound the Instance#
import { effectScope, onMounted } from 'vue'
export default defineAsyncComponent({
async setup() {
// get and hold the instance before `await`
const instance = getCurrentInstance()
// create the scope before `await`, so it will be bond to the instance
const scope = effectScope()
const data = await someAsyncFunction() // <-----------
onMounted(
() => {},
instance // <--- pass the instance to it
)
scope.run(() => {
/* Use `computed`, `watch`, etc. ... */
})
// the lifecycle hooks will not be available here
}
})
References:
Anthony Fu: https://antfu.me/posts/async-with-composition-api