Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Async Function in Vue3 setup()

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

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.