[resolved] Why using await inside composables cause them to break?

I believe my issue is similar to this https://discord.com/channels/473401852243869706/1141010505482379265 My original use case involved combining multiple composables, in which some composables depend on a value from others which I'd like to await to make sure data is rendered server side. I narrowed the issue to this: /app.vue
<script setup lang="ts">
provide(/* key */ 'message', /* value */ 'hello!')
</script>

<template>
<div>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
provide(/* key */ 'message', /* value */ 'hello!')
</script>

<template>
<div>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
/composables/useMessage.ts
export const useMessage = async () => {
await Promise.resolve()
const message = inject('message', 'default-hello!')
return computed(() => message)
}
export const useMessage = async () => {
await Promise.resolve()
const message = inject('message', 'default-hello!')
return computed(() => message)
}
/pages/index.vue
<script setup lang="ts">
const message = await useMessage()
</script>

<template>
<div>
<pre dir="ltr">{{ JSON.stringify({ message }, null, 2) }}</pre>
</div>
</template>
<script setup lang="ts">
const message = await useMessage()
</script>

<template>
<div>
<pre dir="ltr">{{ JSON.stringify({ message }, null, 2) }}</pre>
</div>
</template>
Running the app with the above code causes: * a WARN [Vue warn]: inject() can only be used inside setup() or functional components. * The value of 'message' is undefined Now, if I omit the await Promise.resolve() line from the composable. Everything works as expected including the value of 'message' being 'hello!'. Could you explain why does this happen? I can't make sense of it, because in my understanding this shouldn't be different than a synchronous statement.
3 Replies
kapa.ai
kapa.ai3w ago
Hi @amazonaffected. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Your question is about a specific issue with using await inside Nuxt composables, which is indeed a common problem in Nuxt 3. I'll explain why this happens based on the information provided in the knowledge sources. The core reason for this behavior is related to how Vue and Nuxt handle the context during asynchronous operations. As explained in the Nuxt documentation (https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables): "During a component lifecycle, Vue tracks the temporary instance of the current component (and similarly, Nuxt tracks a temporary instance of nuxtApp) via a global variable, and then unsets it in same tick. This is essential when server rendering, both to avoid cross-request state pollution (leaking a shared reference between two users) and to avoid leakage between different components." When you use await inside a composable, it creates an asynchronous break in the execution. After this break, the Vue/Nuxt context is no longer available, which is why inject() fails to work correctly. This is further explained in the Nuxt documentation (https://nuxt.com/docs/api/composables/use-nuxt-app#a-deeper-explanation-of-context): "The Composition API and Nuxt Composables are only available during lifecycle and in same tick before any async operation." To illustrate this, the documentation provides an example:
async function setup() {
getCurrentInstance() // Works
await someAsyncOperation() // Vue unsets the context in same tick before async operation!
getCurrentInstance() // null
}
async function setup() {
getCurrentInstance() // Works
await someAsyncOperation() // Vue unsets the context in same tick before async operation!
getCurrentInstance() // null
}
In your case, await Promise.resolve() is causing this context loss, even though it's a minimal async operation. To solve this issue, there are several approaches you can take: 1. Avoid using await in composables. This is the simplest solution but may not always be feasible. 2. Use runWithContext or callWithNuxt to restore the context. As mentioned in the Nuxt documentation (https://nuxt.com/docs/api/composables/use-nuxt-app#solution), these functions can be used to restore the context after an async operation. 3. Use the experimental asyncContext feature. This is mentioned in the Nuxt documentation (https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables), but it's still experimental and may not be suitable for production use. 4. Restructure your code to perform async operations last, as suggested by Anthony Fu and mentioned in the YouTube video (https://www.youtube.com/watch?v=ofuKRZLtOdY) about this issue. For your specific use case, you might want to consider restructuring your composable to avoid the await or use runWithContext if you absolutely need the async operation. Remember that this issue is a known limitation in the current design of Vue and Nuxt composables, and the Nuxt team is working on improving this in future versions. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@1188032231105630269> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
amazonaffected
amazonaffectedOP3w ago
Impressive! This pretty much answered everything I wanted to know regarding this issue. Great job to everyone working on this AI / Knowledge base, thank you! I'm not sure if this will work, but I will try to use all await statements at the end in compination with ref() / computed() values.
Want results from more Discord servers?
Add your server