Sí, puedes usar await en el nivel superior de <script setup>. El compilador envuelve el bloque setup en una función async setup() automáticamente. El problema: los componentes asíncronos requieren un límite <Suspense> en el padre para gestionar el estado pendiente mientras el await se resuelve. Sin <Suspense>, el componente nunca se renderiza. Nuxt envuelve cada página en <Suspense> por defecto, así que en Nuxt puedes usar await libremente en páginas y layouts sin preocuparte por ello.
Ejemplo básico
<!-- UserProfile.vue -->
<script setup>
const response = await fetch('/api/user/1')
const user = await response.json()
</script>
<template>
<h1>{{ user.name }}</h1>
</template>Esto compila a:
export default {
async setup() {
const response = await fetch('/api/user/1')
const user = await response.json()
return { user }
}
}El componente es ahora un componente asíncrono. Vue suspende el renderizado hasta que setup se resuelve.
El requisito de Suspense
Un componente asíncrono debe tener un ancestro <Suspense>. Sin él, el componente queda en estado pendiente indefinidamente:
<!-- Parent.vue -->
<template>
<!-- MAL: sin Suspense → UserProfile nunca aparece -->
<UserProfile />
<!-- BIEN: Suspense gestiona el estado pendiente -->
<Suspense>
<UserProfile />
<template #fallback>
<p>Loading profile...</p>
</template>
</Suspense>
</template>El slot #fallback se renderiza mientras el setup asíncrono está pendiente. Una vez que el await se resuelve, Vue intercambia el contenido real.
En Nuxt: funciona sin configuración adicional
Nuxt envuelve cada componente de página en <Suspense> automáticamente a través de <NuxtPage>. No necesitas añadir <Suspense> tú mismo:
<!-- pages/users/[id].vue — página Nuxt -->
<script setup>
const route = useRoute()
const { data: user } = await useFetch(`/api/users/${route.params.id}`)
</script>
<template>
<h1>{{ user.name }}</h1>
</template>Esto funciona sin configuración adicional porque el app.vue de Nuxt contiene <NuxtPage>, que internamente proporciona el límite <Suspense>. Durante SSR, el await se resuelve en el servidor. Durante la navegación del cliente, Nuxt muestra un indicador de carga mientras el setup de la nueva página se resuelve.
El problema: watchers y lifecycle hooks después de await
Cuando usas await, cualquier código después de él se ejecuta en una microtarea diferente. El contexto de getCurrentInstance() de Vue puede perderse. Esto significa que los watchers y lifecycle hooks registrados después de un await pueden no vincularse correctamente:
<script setup>
// Estos funcionan: registrados antes de cualquier await
const count = ref(0)
watch(count, (val) => console.log(val))
onMounted(() => console.log('mounted'))
const data = await fetch('/api/data').then(r => r.json())
// Estos PUEDEN NO funcionar: el contexto de getCurrentInstance() puede perderse
watch(data, (val) => console.log(val)) // no fiable
onMounted(() => console.log('after await')) // no fiable
</script>La regla: registra todos los watchers, lifecycle hooks y composables ANTES del primer await. Pon las declaraciones reactivas al principio, las operaciones asíncronas al final.
<script setup>
// 1. Todo el estado reactivo y composables primero
const count = ref(0)
const items = ref([])
watch(count, (val) => console.log(val))
onMounted(() => console.log('mounted'))
// 2. Operaciones asíncronas al final
const response = await fetch('/api/data')
items.value = await response.json()
</script>Cuándo usar await frente a useFetch
En Nuxt, prefiere useFetch sobre await fetch() directo:
<script setup>
// Preferible: gestiona payload SSR, caché y cancelación
const { data } = await useFetch('/api/users')
// Evitar: sin transferencia de payload, doble fetch en la hydration
const data = ref(await fetch('/api/users').then(r => r.json()))
</script>Ambos usan await, pero useFetch se integra con el sistema de payload de Nuxt. El fetch directo se ejecuta de nuevo en el cliente durante la hydration.
Cuándo NO usar await en setup
Datos que cargan después de una interacción del usuario: usa $fetch en manejadores de eventos o useFetch con immediate: false.
Datos de varias fuentes independientes: las cargas paralelas son mejores que los awaits secuenciales.
<script setup>
// MAL: secuencial — tiempo total = A + B
const users = await useFetch('/api/users')
const posts = await useFetch('/api/posts')
// BIEN: paralelo — tiempo total = max(A, B)
const [users, posts] = await Promise.all([
useFetch('/api/users'),
useFetch('/api/posts')
])
</script>Resumen
| Contexto | ¿Funciona await en setup? | ¿Se necesita Suspense? |
|---|---|---|
| Página/layout Nuxt | Sí | No (automático) |
| Componente Nuxt dentro de una página | Sí | Sí (añade Suspense en el padre) |
| Vue sin Nuxt | Sí | Sí (debes añadirlo tú) |
| Dentro de un manejador de eventos | No aplica (usa $fetch) | No |
Ver también: ¿Cuáles son los lifecycle hooks en Vue 3? · ¿Qué es script setup?
Referencias
- <script setup> - Vue.js docs
- Suspense - Vue.js docs
- getCurrentInstance - Vue.js docs