42

(This question has been answered for JavaScript, see below, but this question is specific for TypeScript, which behaves differently)

I'm trying to use async functionality in Vue3.0 using typescript.

Without async this code works nice:

// file: components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script lang="ts">
import {defineComponent} from 'vue'

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  async setup() { // <-- this works without 'async'
    const test = 'test'

    // await doSomethingAsynchronous()

    return {
      test,
    }
  },
})
</script>

With async setup() the component "HelloWorld" disappears from the page, and the Firefox console tells me

"Uncaught (in promise) TypeError: node is null (runtime-dom.esm-bundler.js)"

When I change async setup() to setup(), the code works, but then I would not be able to use async/await inside the setup function.

So my question: how do I use async/await inside the setup() function using Typescript?

EDIT:

The answer to this question: why i got blank when use async setup() in Vue3 shows that async setup() does work with JavaScript, so I would expect it to work in TypeScript as well.

Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
Hendrik Jan
  • 4,396
  • 8
  • 39
  • 75

4 Answers4

58

Try to use onMounted hook to manipulate asynchronous call :

 setup() {
    const users = ref([]);
    onMounted(async () => {
      const res = await axios.get("https://jsonplaceholder.typicode.com/users");
      users.value = res.data;
      console.log(res);
    });

    return {
      users,
    };
  },

LIVE DEMO

According to official docs the Best approach is to use async setup in child component and wrap that component by Suspense component in the parent one :

UserList.vue

<script  lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
    async setup() {
        //get users from jsonplacerholder using await fetch api 
        const users = await fetch("https://jsonplaceholder.typicode.com/users").then(res => res.json());

        return {
            users
        }
    }
})
</script>
<template>
    <div>
        <!-- list users -->
        <ul>
            <li v-for="user in users">{{ user.name }}</li>
        </ul>
    </div>
</template>

Parent component:

<script lang="ts">

import UserList from "../components/tmp/UserList.vue";
...
</script>

   <div>
            <!-- Suspense component  to show users  -->
            <Suspense>
                <template #fallback>
                    <div>loading</div>
                </template>

                <UserList />
            </Suspense>
        </div>

Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
  • 3
    Why is that the 'best approach'? – Lee Goddard Aug 30 '22 at 07:21
  • 1
    @LeeGoddard I guess that's because it is the recommended approach from Vue documentation. https://vuejs.org/api/composition-api-setup.html#basic-usage – Wilson Dec 11 '22 at 03:16
  • Such information might be best placed within the answer. – Lee Goddard Dec 12 '22 at 21:37
  • 3
    Great answer. However, as of January 2023, the doc says Suspense is an experimental feature, which means it probably isn't the best. – Joshua Jan 19 '23 at 17:14
  • Agree with @joshua is an experimental feature and its API will likely change. – Simon Klimek Apr 12 '23 at 10:12
  • 1
    @SimonKlimek it can change internally but it will keep consumed in the same way, I see Evan You said it will be stable very soon – Boussadjra Brahim Apr 12 '23 at 10:34
  • @BoussadjraBrahim Can you help point us to where he said that? I couldn't find anything with a quick Google. Meanwhile the official Vue docs at https://vuejs.org/guide/built-ins/suspense.html still warn: "It is not guaranteed to reach stable status and the API may change before it does." I personally wouldn't use it until that's updated. – Joshua Apr 13 '23 at 14:04
  • https://youtu.be/I5mGNB-4f0o?t=1094 – Boussadjra Brahim Apr 13 '23 at 14:21
24

Another way of doing this:

 const users = ref([]);
 
 (async () => {
   const res = await axios.get("https://jsonplaceholder.typicode.com/users");
   users.value = res.data;
   console.log(res);
 })()

 return {
   users,
 }

And you don't have to wait for it to mount, this is similar to using created() with the options API.

Note: Don't forget to always have a semicolon ";" before the function statement, otherwise, JavaScript would think that the previous statement was supposed to return a function, the following code, for example, would cause a bug "ref([]) is not a function":

const users = ref([]) // No semicolon here

(async () => {

Another way of preventing this bug is to always have the semicolon on the same line of the function definition, the following code also works:

;(async () => {
gustavodacrvi
  • 255
  • 3
  • 5
  • 1
    Much cleaner! And beware not to put the executing brackets inside the grouping brackets: you will get a TS1128: Declaration or statement expected. – Onno van der Zee Jun 28 '21 at 14:18
  • 2
    When you create a immediately invoked function you split the thread into two running in parallel. This answer will always return an empty users and THEN update it. You should be using suspense and async setup instead. Unless you want this buggy experience of updating the UI. – basickarl Feb 23 '22 at 19:00
  • 1
    Keep in mind that suspense is still considered an "Experimental feature" though. https://vuejs.org/guide/built-ins/suspense.html – gustavodacrvi Mar 18 '22 at 14:56
8

To call API even before the beforeCreate() lifecycle hook, there're a couple of ways:

  1. Use a helper function (more elegant than ;(async () => {})()):
// utils.ts
export const run = (asyncFn: () => Promise<void>) => asyncFn()
// component.vue
<script lang="ts" setup>
  import { ref } from 'vue'
  import { run } from './utils.ts'
  import { getUsers } from './api.ts'

  const users = ref([])

  run(async () => {
    // getUsers() gets called before beforeCreate()
    users.value = await getUsers()
    // ...
  })
</script>
  1. Use promise chaining:
// component.vue
<script lang="ts" setup>
  import { ref } from 'vue'
  import { getUsers } from './api.ts'

  const users = ref([])

  // getUsers() gets called before beforeCreate()
  getUsers().then(users => {
    users.value = users
    // ...
  })
</script>
Wenfang Du
  • 8,804
  • 9
  • 59
  • 90
  • same problem as with @gustavodacrvi answer. In fact, even though the code runs before the `beforeCreate`, the data is resolved later than `onMounted` because any api call needs time and setup will not wait. It seems the closest "standard" thing to call api before mounting is to call it in async `onBeforeMount` – Niksr Jan 15 '23 at 13:52
  • 1
    I can say further, even though commands inside async `onBeforeMount` will start in the proper moment, they again will be finished after the entire setup will be completed and the page will be mounted. And it's visible as "flashing". So it seems there is no way to do anything inside synchronous setup what should be done before the page will be mounted. Either to use their "experimental" suspense or to execute the code in the `beforeEnter` router guard. The latter one will not fire if the params are just changed (e.g. user Id) so I basically don't know what to do. – Niksr Jan 16 '23 at 18:57
  • 1
    @Niksr In most use cases, there's no point for the request to must finish before `mounted`. – Wenfang Du Jun 04 '23 at 06:45
  • You're right. No point to stop rendering to await API. Proper way to show e.g. placeholder or spinner and then replace it with actual data as soon as it was received. – Niksr Jun 05 '23 at 18:45
-1

Here is another way. It's very similar to gustavodacrvi' answer, but if you don't like self-invoking functions as much as I do, you may find this version a bit nicer:

<script setup lang="ts">
const users = ref<User[]>([])
const isLoading = ref(true)

async function fetchUsers() {
  users.value = await getUsers()
  isLoading.value = false
}

fetchUsers()
</script>

This requires you to take loading state into account, because users array will initially be empty and then updated.

Romalex
  • 1,582
  • 11
  • 13