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: hidden aplicado em containers pais (que corta o modal).
  • Propriedades CSS como transform, filter ou perspective que criam um novo contexto de empilhamento (stacking context), fazendo com que um z-index: 9999 perca 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 &lt;body&gt;, 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>