Capítulo 04: Propriedades Computadas (computed) e Observadores (watch, watchEffect)

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


🗺️ Mapa Conceitual de Derivações e Efeitos Colaterais

flowchart TD
    A["Mutações em Estados Reativos (Refs / Reactive)"] --> B{"Natureza da Operação Desejada"}
    B -->|"Derivação Pura de Dado (Com Caching)"| C["computed()"]
    B -->|"Efeito Colateral Explícito (Async/I/O)"| D["watch(source, callback)"]
    B -->|"Efeito com Rastreamento Automático"| E["watchEffect(callback)"]

    C --> F["Retorna Readonly Ref com Flag 'dirty'"]
    D --> G["Recebe (novoValor, antigoValor)"]
    E --> H["Executa imediatamente e rastreia deps"]

    G & H --> I["onCleanup: Aborta timers / requisições pendentes"]
    F --> J["Reutiliza Cache até dependência mudar"]

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

🏛️ 1. Derivações Puras vs Efeitos Colaterais: A Arquitetura Reativa

No desenvolvimento front-end com Vue 3, separar derivações puras de dados de efeitos colaterais (side effects) é essencial para evitar bugs de sincronização, vazamentos de memória e re-renderizações infinitas.

1.1 computed(): Memoização e Flag Suja (Dirty Checking)

Uma propriedade computada (computed) encapsula uma computação puramente declarativa:

  • Memoização Inteligente: O Vue calcula o valor derivado e armazena o resultado em cache. Leituras subsequentes retornam o valor em cache instantaneamente em complexidade $O(1)$.
  • Dirty Checking Reativo: Quando qualquer dependência reativa associada muda, o Vue não recalcula a função imediatamente; ele apenas marca a flag interna dirty = true. O recálculo real só acontece quando a propriedade for efetivamente lida novamente por uma função de renderização ou outro consumidor ativo (Lazy Evaluation).
  • Writable Computed: Permite expor uma interface de escrita através de um objeto com métodos get() e set(novoValor).

1.2 watch(): Observação Explícita e Controle Fino

O watch destina-se a disparar operações assíncronas (chamadas HTTP, manipulação de APIs do navegador como localStorage, disparos de métricas) em resposta à mudança de uma fonte específica:

  • Fontes Válidas: Uma Ref, uma função getter que retorna um valor (() => state.user.id), ou um array de fontes ([userId, role]).
  • Opções Avançadas:
    • immediate: true: Executa a função de callback imediatamente na montagem do componente.
    • deep: true: Percorre recursivamente todas as propriedades aninhadas de um objeto.
    • flush: 'post': Aguarda o Vue atualizar o DOM antes de executar o callback, permitindo inspecionar nós atualizados com segurança.

1.3 watchEffect(): Rastreamento Implícito e Cleanup

Diferente do watch, o watchEffect não exige declarar fontes explicitamente. Qualquer ref lida dentro do corpo do callback é automaticamente registrada como dependência. Executa imediatamente no início e aceita um parâmetro onCleanup(callback) para descartar tarefas pendentes (race conditions).


⚖️ 2. Matriz Decisória: computed vs watch vs watchEffect

Critério Arquitetural computed() watch() watchEffect()
Objetivo Primário Derivar novos dados a partir do estado existente. Disparar efeito colateral assíncrono em fonte alvo. Disparar efeito colateral com rastreamento automático.
Possui Cache / Memoização? Sim (fundamental). Não. Não.
Executa na Montagem? Avaliado sob demanda (Lazy). Apenas com { immediate: true }. Sempre executa imediatamente.
Acesso ao Valor Anterior? Não. Sim (newVal, oldVal). Não.
Fontes de Dependência Automáticas dentro da função getter. Explícitas (ref, getter ou array). Implícitas (rastreadas na execução).
Efeitos Assíncronos / HTTP ❌ PROIBIDO (deve ser função pura). ✅ RECOMENDADO. ✅ RECOMENDADO (com onCleanup).

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

Anti-Pattern 1: Efeitos Colaterais ou Mutações dentro de computed()

Erro Comum: Modificar outro estado reativo ou disparar requisições assíncronas dentro de um getter computado:

// ❌ ANTI-PATTERN CRÍTICO
const userFullName = computed(() => {
  api.post('/log', { name }); // Efeito colateral imperdoável em computed!
  isLoggedIn.value = true;    // Muta outro estado durante cálculo derivado!
  return `${user.firstName} ${user.lastName}`;
});

Abordagem Correta: Funções de computed devem ser puras, determinísticas e livres de efeitos colaterais. Para logs e chamadas assíncronas, use watch.

Anti-Pattern 2: Observar Propriedade Primitiva de Objeto sem Getter Function

Erro Comum: Escrever watch(userProfile.age, (newAge) => ...) onde age é um número primitivo dentro de um reactive. Abordagem Correta: Passar uma função getter: watch(() => userProfile.age, (newAge) => ...).


💻 4. Código Conceitual Arquitetural Comentado

<!-- src/components/FinancialCalculator.vue -->
<script setup lang="ts">
import { ref, computed, watch, watchEffect } from 'vue';

// 1. Estados Reativos de Entrada
const rawAmount = ref<number>(1000);
const taxRate = ref<number>(0.15); // 15%
const queryParam = ref<string>('');

// 2. Computed Pura com Cache Memoizado
const calculatedTax = computed<number>(() => {
  return rawAmount.value * taxRate.value;
});

// 3. Writable Computed com Getter e Setter Bidirecionais
const totalWithTax = computed<number>({
  get: () => rawAmount.value + calculatedTax.value,
  set: (newTotal: number) => {
    // Reverte a base a partir do novo total desejado
    rawAmount.value = Number((newTotal / (1 + taxRate.value)).toFixed(2));
  }
});

// 4. Watch com Debounce e Limpeza de Race Conditions (onCleanup)
watch(queryParam, (term, _, onCleanup) => {
  if (!term.trim()) return;

  const timer = setTimeout(() => {
    console.log(`[API Async Watch] Buscando relatórios fiscais para: ${term}`);
  }, 400);

  // Cancela o timer se o usuário digitar uma nova tecla antes de 400ms
  onCleanup(() => clearTimeout(timer));
});

// 5. WatchEffect com Atualização Automática de Telemetria
watchEffect(() => {
  console.log(`[Audit] Total Fiscal recalculado: R$ ${totalWithTax.value.toFixed(2)}`);
});
</script>

<template>
  <div class="max-w-md mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-4">
    <h3 class="font-bold text-slate-800 text-lg">Calculadora Fiscal Corporativa</h3>

    <div class="space-y-2">
      <label class="block text-xs font-semibold text-slate-600">Valor Base (R$):</label>
      <input type="number" v-model.number="rawAmount" class="w-full border px-3 py-2 rounded-lg text-sm" />
    </div>

    <div class="p-4 bg-slate-50 border rounded-xl space-y-1 text-sm">
      <div class="flex justify-between text-slate-600">
        <span>Imposto (15%):</span>
        <strong class="text-slate-800">R$ {{ calculatedTax.toFixed(2) }}</strong>
      </div>
      <div class="flex justify-between text-base font-bold text-indigo-600 border-t pt-2">
        <span>Total com Imposto:</span>
        <span>R$ {{ totalWithTax.toFixed(2) }}</span>
      </div>
    </div>
  </div>
</template>

🛠️ 5. Cheat-Sheet de Sintaxes: computed e watch

// Computed somente leitura (padrão)
const dobro = computed(() => valor.value * 2);

// Computed bidirecional (Writable)
const valorCustom = computed({
  get: () => valor.value,
  set: (val) => { valor.value = val; }
});

// Watch em uma única Ref
watch(meuRef, (novo, antigo) => { ... });

// Watch em propriedade de reactive com Getter
watch(() => obj.prop, (novo, antigo) => { ... });

// Watch com opções Immediate e Deep
watch(filtro, callback, { immediate: true, deep: true });

// WatchEffect com Cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController();
  fetch('/api/dados', { signal: controller.signal });
  onCleanup(() => controller.abort());
});