Subscribe on changes!

Better support for top-level await in <script setup>

avatar
Nov 17th 2021

What problem does this feature solve?

Currently a top-level await in a <script setup> block causes the template not to render, see this StackOverflow question. Although there are various workarounds, using top-level await is the most natural solution to the common "fetch data from the REST API and display it in the component when it loads" problem.

What does the proposed API look like?

Perhaps we can simply compile a top-level $await in a non-Suspense component to a Promise.then(), so that this snippet

<script setup>
let data = ref('')
data.value  = $await getDataFromApi()
</script>

would be compiled to

<script setup>
let data = ref('')
getDataFromApi().then(d => data.value = d)
</script>

Disclaimer: this is just a wild idea, there may be caveats that I'm unable to see at this moment and there may be better ways to achieve this.

avatar
Nov 17th 2021

This is inherent to the language, in your scenario, using a Promise is the right approach, even inside of setup():

async setup() {
  data.value = await stuff()
  return { data }
}

is different from

setup() {
  stuff().then(d = > data.value = d)
  return { data }
}

Magically making it work only when the await is at the very end would change the behavior from blocking to non blocking. Note that the component doesn't know if it will be wrapped by Suspense or not when its template is compiled.

avatar
Nov 17th 2021

Alright, but can we make the developer experience better in some other way? Should using Promise.then() be documented as the recommended solution to this common problem?

avatar
Nov 17th 2021

No because they are just different and valid behaviors. What is interesting is to have https://github.com/vuejs/vue-next/issues/2603

avatar
Nov 18th 2021

Alright, let me just point out that there are at least two other recent questions in similar vein in Stack Overflow, so this is seems to be confusing developers:

  1. https://stackoverflow.com/questions/69903520/vue-export-import-async-await-stops
  2. https://stackoverflow.com/questions/64117116/how-can-i-use-async-await-in-the-vue-3-0-setup-function-using-typescript

Folks suggest to use Suspense as the solution, which complicates code unnecessarily in my opinion for this simple and common case.

avatar
Nov 18th 2021

Top level awai compiles to async setup() and that requires Suspense. All of this is documented here, and would be apparent to users if we had something like #2603. So this is a question of DX that we have to solve first and foremost.

Top-Level await is not a simple thing to support because what will be rendered while the component is being awaited? Does the whole app stop rendering? Does it continue and return to this component once it's done? Wouldn'T you want a placeholder while it's loading? How to display that? etc. pp.

Suspense is the experiment meant to make that possible. Without it, a top-level await in a setup (function) won't be able to work in a way that is useful in a real application.

avatar
Nov 26th 2021

Yes, my original proposal is too radical. Nevertheless, I think we should provide a reasoned recommendation for the common "fetch data from a REST API, show a spinner while the request executes and display the result in the component when it is done" case. There are too many options at the moment which is confusing.

Without async components and Suspense

In all cases that follow, assume the following template:

<template>
  <p v-if="error">{{error}}</p>
  <p v-if="!data">Loading...</p>
  <p v-else>Data: {{data}}</p>
</template>

and the following script block head:

<script setup>
  const data = ref('');
  const error = ref('');
  1. You can use either onBeforeMount:

    onBeforeMount(async () => {
      try {
        data.value = await getDataFromApi();
      } catch (e) {
        error.value = e;
      }
    })
    
  2. or do the initialization inside an async function that you invoke without awaiting for the promise to complete:

    const init = async () => {
      try {
        data.value = await getDataFromApi();
      } catch (e) {
        error.value = e;
      }
    }
    init();
    
  3. or use promise chaining:

    getDataFromApi()
      .then(d => data.value = d)
      .catch(e => error.value = e);
    

With async components and Suspense

ShowData.vue:

<template>
  <p v-if="error">{{error}}</p>
  <p v-else>Data: {{data}}</p>
</template>
<script setup>
const data = ref('');
const error = ref('');
try {
  data.value = await getDataFromApi();
} catch (e) {
  error.value = e;
}
</script>

App.vue:

<template>
  <suspense>
    <template #default>
      <ShowData />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </suspense>
</template>

<script>
import { defineAsyncComponent } from "vue";

export default {
  name: "App",
  components: {
    HelloWorld: defineAsyncComponent(() =>
      import("./components/ShowData.vue")
    ),
  },
};
</script>

I would argue that using async components and Suspense is not justified for only asynchronous data fetching, but please prove me wrong.

avatar
Nov 27th 2021

All of these "too many options" existed in a similar form ever since Vue was created. adding top-level await would also not remove these 3 existing ways, it would add another one on top, so I don't really see an argument for top-level await in that reasoning specifically.

What you are arguing for is a 4th way that is more ergonomic/elegant than the 3 existing ones - which absolutely is a fine goal to have, but it won't reduce the number of way to do things, it would increase them.

On Suspense: It is not meant to solve the challenge of local async data fetching. it's meant to solve the problem of yank resulting from 10 different places on a page re-rendering uncoordinated at different moments as they are all replacing their local "loading" placeholder whenever their own data has been fetched.

So Suspense exists, and does so for a reason different from the challenge you want to see addressed. And top-level await is currently coupled to Suspense. So if we wanted to repurpose top-level await to rather be used for local datafetching without Suspense, we would have to figure how to use Suspense, then.

Which means there's a lot to be explored and discussed. And that solution would have to go through the vuejs/rfcs process - feel free to start a free-form discussion thread there to start exploration.

avatar
Nov 28th 2021

What you are arguing for is a 4th way that is more ergonomic/elegant than the 3 existing ones - which absolutely is a fine goal to have, but it won't reduce the number of way to do things, it would increase them.

Thank you for expressing my intent so well. Yes, this is my goal. However, I would argue that the fourth way (if we find it) will not scatter the landscape even more but rather provide the obvious, recommended way of doing things (think of Python's there should be one obvious way to do it). Elegant idioms will usually prevail - like SFCs, the composition API and <script setup> have become the obvious, preferred way of building Vue apps.

Unfortunately I don't have a good proposal at the moment, the initial one was just a wild idea. But I think we should somehow address this, if only in the docs.

avatar
Aug 25th 2022

This is inherent to the language, in your scenario, using a Promise is the right approach, even inside of setup():

async setup() {
  data.value = await stuff()
  return { data }
}

is different from

setup() {
  stuff().then(d = > data.value = d)
  return { data }
}

Magically making it work only when the await is at the very end would change the behavior from blocking to non blocking. Note that the component doesn't know if it will be wrapped by Suspense or not when its template is compiled.

I think some attribute can easily fix this issue, for example:

<script setup async>
  data.value = await stuff()
  return { data }
</script>