Capítulo 06: Ciclo de Vida na Composition API (onMounted, onUnmounted)

Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript


🗺️ Mapa Conceitual do Ciclo de Vida de um Componente Vue 3

flowchart TD
    A["Inicialização do Componente"] --> B["setup() / <script setup> (Executa no Server e Client)"]
    B --> C["Criação e Compilação do Virtual DOM"]
    C --> D["onMounted (Client-Only: DOM Real Disponível)"]

    D --> E{"Mutações Reativas (State Change)"}
    E -->|Sim| F["onBeforeUpdate -> Reconciliação do VDOM -> onUpdated"]
    F --> E

    E -->|Destruição / Navegação| G["onBeforeUnmount"]
    G --> H["onUnmounted (Client-Only: Limpeza de Timers e Listeners)"]

    subgraph ERROR_HANDLING["Gestão de Exceções"]
        I["onErrorCaptured: Intercepta erros de filhos"]
    end

    B & D & F -.-> ERROR_HANDLING

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style H fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style ERROR_HANDLING fill:#ede7f6,stroke:#7e57c2,stroke-width:2px

🏛️ 1. O Ciclo de Vida na Era da Composition API

Na Composition API, os hooks de ciclo de vida são importados diretamente como funções independentes (import { onMounted, onUnmounted } from 'vue'). Ao contrário da Options API, onde existiam opções únicas (mounted() { ... }), na Composition API você pode invocar o mesmo hook múltiplas vezes dentro de funções ou composables separados, promovendo alto grau de modularidade.

1.1 setup() vs onMounted: A Fronteira entre Servidor e Navegador

  • Corpo de <script setup>: Equivale aos antigos beforeCreate e created. Executa tanto no servidor (durante o Server-Side Rendering do Nuxt 3) quanto no navegador do cliente. Nunca acesse window, document ou APIs de terceiros que dependem de DOM diretamente no nível raiz de setup.
  • onMounted: Disparado exclusivamente no cliente logo após todos os elementos do componente e seus nós do DOM filhos terem sido inseridos no documento HTML. É o local seguro para instanciar gráficos (Chart.js), mapas (Leaflet), focar inputs ou registrar eventos globais de tela.

1.2 onUnmounted e Prevenção de Memory Leaks

Em Single Page Applications (SPAs), quando o usuário navega entre páginas, os componentes antigos são destruídos, mas o JavaScript da aplicação continua em execução na memória. Qualquer timer (setInterval), WebSocket, observador de redimensionamento (ResizeObserver) ou listener (window.addEventListener) não removido em onUnmounted causará Memory Leak e degradação contínua de performance.

1.3 nextTick() e o Ciclo Assíncrono do DOM

O Vue não atualiza o DOM instantaneamente no momento em que um ref é modificado. Ele enfileira as mutações em um buffer e as aplica em lote na próxima microtarefa do navegador (batched updates).

  • Se você modificar um estado e precisar ler imediatamente as novas dimensões de um nó do DOM (ex: element.scrollHeight), você deve aguardar com await nextTick().

⚖️ 2. Tabela de Correspondência: Options API vs Composition API

Options API (Vue 2 / Legado) Composition API (Vue 3) Ambiente de Execução Finalidade Principal
beforeCreate / created Corpo de <script setup> Servidor + Cliente Inicialização de estado reativo e injeção de dependências.
beforeMount onBeforeMount Apenas Cliente Imediatamente antes da inserção física no DOM.
mounted onMounted Apenas Cliente Acesso garantido ao DOM, inicialização de plugins externos.
beforeUpdate / updated onBeforeUpdate / onUpdated Apenas Cliente Notificação pós-reconciliação de nós do DOM.
beforeUnmount / unmounted onBeforeUnmount / onUnmounted Apenas Cliente Limpeza de recursos, timers, sockets e listeners globais.
errorCaptured onErrorCaptured Servidor + Cliente Barreira de contenção de erros de componentes descendentes.

⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)

Anti-Pattern 1: Acessar Objeto window ou document na Raiz do <script setup>

Erro Comum: Escrever const width = window.innerWidth; solto no script. Durante a renderização no servidor (SSR / Nuxt), o Node.js/Nitro lançará ReferenceError: window is not defined, derrubando o servidor. Abordagem Correta: Mova o acesso para dentro de onMounted(() => { ... }) ou proteja com if (typeof window !== 'undefined').

Anti-Pattern 2: Esquecer de Remover Handlers de Eventos Globais

Erro Comum: window.addEventListener('keydown', handleKey) dentro de onMounted sem chamar window.removeEventListener('keydown', handleKey) em onUnmounted. Cada montagem do componente acumulará um listener zumbi duplicado na memória.


💻 4. Código Conceitual Arquitetural Comentado

<!-- src/components/SafeDomObserver.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue';

const messageList = ref<string[]>(['Sistema inicializado.']);
const chatBoxRef = ref<HTMLDivElement | null>(null);

let heartbeatTimer: ReturnType<typeof setInterval> | null = null;

// Executado com segurança apenas no navegador
onMounted(() => {
  console.log('[SafeDomObserver] Montado no cliente. Registrando listeners.');
  window.addEventListener('resize', handleResize);

  heartbeatTimer = setInterval(() => {
    addMessage(`Heartbeat de sincronia: ${new Date().toLocaleTimeString()}`);
  }, 3000);
});

// Limpeza cirúrgica contra Memory Leaks
onUnmounted(() => {
  console.log('[SafeDomObserver] Desmontado. Limpando timers e listeners globais.');
  window.removeEventListener('resize', handleResize);
  if (heartbeatTimer) clearInterval(heartbeatTimer);
});

function handleResize(): void {
  console.log(`[Viewport] Dimensão: ${window.innerWidth}x${window.innerHeight}`);
}

async function addMessage(text: string): Promise<void> {
  messageList.value.push(text);

  // Aguarda o DOM renderizar o novo elemento antes de medir scrollHeight
  await nextTick();
  if (chatBoxRef.value) {
    chatBoxRef.value.scrollTop = chatBoxRef.value.scrollHeight;
  }
}
</script>

<template>
  <div class="max-w-md mx-auto p-5 bg-white border rounded-2xl shadow-sm space-y-3">
    <h3 class="font-bold text-slate-800 text-base">Terminal de Eventos do Sistema</h3>

    <!-- Template Ref associada ao container HTML -->
    <div
      ref="chatBoxRef"
      class="h-40 bg-slate-900 text-emerald-400 p-3 rounded-xl font-mono text-xs overflow-y-auto space-y-1"
    >
      <div v-for="(msg, idx) in messageList" :key="idx">{{ msg }}</div>
    </div>
  </div>
</template>

🛠️ 5. Cheat-Sheet dos Hooks de Ciclo de Vida

import { onMounted, onUpdated, onUnmounted, onErrorCaptured, nextTick } from 'vue';

// Montagem no cliente (Acesso seguro ao DOM)
onMounted(() => {
  // Inicialização de bibliotecas externas (Leaflet, D3, etc.)
});

// Desmontagem e desalocação de recursos
onUnmounted(() => {
  // clearInterval(), abortController.abort(), removeEventListener()
});

// Aguardar sincronização da microtarefa do DOM
await nextTick();

// Captura e tratamento de erros de filhos (Error Boundary)
onErrorCaptured((err, instance, info) => {
  console.error('Falha capturada:', err);
  return false; // Interrompe propagação
});