Capítulo 12: Teleport e Suspense para Componentes Assíncronos
Capítulo 12: Teleport e Suspense para Componentes Assíncronos
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual de Teleport e Orquestração Assíncrona
flowchart TD
subgraph ARVORE_LOGICA["Árvore de Componentes (Vue VDOM)"]
A["DashboardView"] --> B["<Suspense>"]
B --> C["AsyncTable (Top-level await)"]
A --> D["<Teleport to='body'>"]
D --> E["ModalDialog (Estado local)"]
end
subgraph DOM_REAL["Renderização Física no DOM do Navegador"]
F["#app -> <main> (Área Principal)"]
G["#app -> Skeleton Fallback (Enquanto AsyncTable carrega)"]
H["<body> -> Diálogo Modal (Fora de qualquer container)"]
end
C -.->|"Promessa Resolvida"| F
B -.->|"Aguardando Promessa"| G
E -->|"Desacoplamento Visual"| H
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style D fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style H fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. Desacoplamento Físico e Carregamento Assíncrono
O Vue 3 introduz dois componentes nativos poderosos projetados para resolver problemas crônicos de arquitetura front-end: o <Teleport> (que liberta nós HTML de restrições de posicionamento) e o <Suspense> (que unifica a orquestração de dependências assíncronas).
1.1 <Teleport>: Quebrando o Stacking Context do CSS
Em interfaces ricas, elementos como diálogos modais, popovers, tooltips e drawers frequentemente sofrem com:
overflow: hiddenaplicado em containers pais (que corta o modal).- Propriedades CSS como
transform,filterouperspectiveque criam um novo contexto de empilhamento (stacking context), fazendo com que umz-index: 9999perca para elementos irmãos fora do container. O<Teleport to="body">transfere fisicamente a renderização do nó HTML para o elemento<body(ou qualquer seletor CSS válido), mantendo a árvore lógica de eventos e reatividade no mesmo componente pai.
1.2 Top-Level await em <script setup>
Na Composition API, você pode utilizar a palavra-chave await diretamente no corpo de nível superior de um <script setup>:
<script setup lang="ts">
// Top-Level Await nativo: transforma o componente em uma dependência assíncrona
const user = await fetchUserProfile();
</script>
O componente pai não consegue montar esse componente filho diretamente com renderização síncrona; ele exige um envelope <Suspense>.
1.3 <Suspense>: Orquestração de Slots Default e Fallback
O componente <Suspense> encapsula um ou mais componentes assíncronos e gerencia dois estados declarativos através de slots nomeados:
#default: O componente assíncrono real, renderizado assim que todas as Promises pendentes forem resolvidas.#fallback: O esqueleto de carregamento (skeleton loader ou spinner) exibido na tela enquanto os componentes assíncronos carregam.
1.4 Barreira de Contenção de Erros com onErrorCaptured
Se qualquer Promise disparada dentro de um componente assíncrono for rejeitada (ex: erro de rede HTTP 500), o <Suspense> não falha silenciosamente; ele propaga o erro para o hook de ciclo de vida onErrorCaptured do ancestral, que pode renderizar uma interface amigável de recuperação (Error Boundary).
⚖️ 2. Tabela Comparativa de Componentes Utilitários
| Componente | Função Primária | Destino no DOM | Tratamento de Erros |
|---|---|---|---|
<Teleport> |
Mudar nó de lugar sem mudar estado. | Qualquer seletor (body, #modal-root). |
N/A (regras normais de VDOM). |
<Suspense> |
Orquestrar dependências assíncronas. | Mantém o fluxo normal do container. | Capturado via onErrorCaptured. |
<KeepAlive> |
Cachear instâncias de componentes. | Mantém o elemento montado em memória. | N/A (reutiliza instância anterior). |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Esquecer o Slot Fallback no <Suspense>
Erro Comum: Declarar <Suspense><AsyncComponent /></Suspense> sem o bloco <template #fallback>. A interface ficará completamente em branco durante a espera assíncrona, dando ao usuário a impressão de que a aplicação travou.
Anti-Pattern 2: Alvo do <Teleport> Inexistente
Erro Comum: Escrever <Teleport to="#meu-container"> quando o ID #meu-container ainda não existe no HTML. No modo cliente puro, o Vue lançará erro fatal se o alvo não for encontrado no DOM antes da montagem.
💻 4. Código Conceitual Arquitetural Comentado
<!-- src/components/AsyncDataDashboard.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue';
import AsyncReportTable from './AsyncReportTable.vue';
const isHelpModalOpen = ref<boolean>(false);
const errorDetails = ref<string | null>(null);
// Captura falhas de rede no carregamento assíncrono
onErrorCaptured((err) => {
errorDetails.value = `Falha na requisição de dados: ${(err as Error).message}`;
return false; // Interrompe propagação do erro
});
</script>
<template>
<div class="max-w-2xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-4">
<div class="flex justify-between items-center border-b pb-3">
<h3 class="font-bold text-slate-800 text-base">Relatório de Transações</h3>
<button
@click="isHelpModalOpen = true"
class="text-xs text-indigo-600 hover:underline font-semibold"
>
Ajuda (Abrir via Teleport)
</button>
</div>
<!-- Tratamento de Erro Boundary -->
<div v-if="errorDetails" class="p-4 bg-red-50 text-red-700 text-xs rounded-xl">
⚠️ {{ errorDetails }}
</div>
<!-- ORQUESTRAÇÃO COM SUSPENSE -->
<Suspense v-else>
<template #default>
<AsyncReportTable />
</template>
<template #fallback>
<div class="p-6 bg-slate-50 border border-dashed rounded-xl text-center text-xs text-slate-400 animate-pulse">
⚡ Carregando registros assíncronos do backend...
</div>
</template>
</Suspense>
<!-- TELEPORT PARA O FINAL DO BODY -->
<Teleport to="body">
<div
v-if="isHelpModalOpen"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
@click.self="isHelpModalOpen = false"
>
<div class="bg-white rounded-2xl p-6 max-w-sm w-full space-y-3 shadow-2xl">
<h4 class="font-bold text-slate-900">Documentação do Relatório</h4>
<p class="text-xs text-slate-600">
Este modal está renderizado fisicamente no final da tag <body>, evitando quaisquer problemas de clipping CSS.
</p>
<button
@click="isHelpModalOpen = false"
class="w-full py-2 bg-indigo-600 text-white rounded-xl text-xs font-bold"
>
Entendido
</button>
</div>
</div>
</Teleport>
</div>
</template>
🛠️ 5. Cheat-Sheet: Teleport e Suspense
<!-- Teleport Simples para Body -->
<Teleport to="body" :disabled="isMobile">
<div class="dialog-box">...</div>
</Teleport>
<!-- Suspense com Slots Default e Fallback -->
<Suspense>
<template #default>
<!-- Componente com top-level await -->
<MeuComponenteAssincrono />
</template>
<template #fallback>
<!-- Skeleton loader durante a espera -->
<SkeletonDashboard />
</template>
</Suspense>