Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 04 — Otimização de Derivações com computed e Efeitos com watch / watchEffect

O exemplo abaixo demonstra a distinção fundamental entre cálculos puros com cache (computed, incluindo suporte a getter/setter bidirecional) e efeitos colaterais assíncronos (watch e watchEffect) com limpeza de recursos (onCleanup).

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

// Estado de entrada de dados
const baseSalary = ref<number>(5000);
const performanceScore = ref<number>(85); // 0 a 100
const searchKeyword = ref<string>('');
const logs = ref<string[]>([]);

// 1. COMPUTED PURA (Leitura com Caching Inteligente)
const bonusPercentage = computed<number>(() => {
  if (performanceScore.value >= 90) return 0.25;
  if (performanceScore.value >= 75) return 0.15;
  if (performanceScore.value >= 50) return 0.05;
  return 0.0;
});

// 2. COMPUTED BIDIRECIONAL (Getter e Setter)
const totalCompensation = computed<number>({
  get: () => {
    return baseSalary.value * (1 + bonusPercentage.value);
  },
  set: (newTotal: number) => {
    // Ajusta o salário-base inversamente
    baseSalary.value = Math.round(newTotal / (1 + bonusPercentage.value));
  }
});

// 3. WATCH ESPECÍFICO (Com valor antigo, novo e opção immediate)
watch(
  performanceScore,
  (newScore, oldScore) => {
    const entry = `[Score Watcher] Alteração de ${oldScore ?? 0} para ${newScore} pontos.`;
    logs.value.unshift(entry);
  },
  { immediate: false }
);

// 4. WATCH COM DEBOUNCE E CLEANUP (Efeito Colateral Assíncrono)
watch(searchKeyword, (term, _, onCleanup) => {
  if (!term.trim()) return;

  const timer = setTimeout(() => {
    logs.value.unshift(`[API Search] Consulta disparada para termo: "${term}"`);
  }, 500);

  // onCleanup cancela o timer anterior se o usuário continuar digitando
  onCleanup(() => {
    clearTimeout(timer);
  });
});

// 5. WATCHEFFECT (Rastreamento Automático de Dependências)
watchEffect((onCleanup) => {
  const currentTotal = totalCompensation.value;
  const currentBase = baseSalary.value;

  const message = `[Audit Sync] Folha recalculada: Base R$ ${currentBase} -> Total R$ ${currentTotal}`;
  // Simulação de persistência em log
  console.log(message);

  onCleanup(() => {
    // Executado imediatamente antes do re-efeito ou desmontagem
  });
});

function adjustTotalManual(): void {
  // Dispara o setter de totalCompensation
  totalCompensation.value = 8000;
}
</script>

<template>
  <div class="max-w-2xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
    <h2 class="text-xl font-bold text-slate-800 border-b pb-3">
      📊 Cálculo Reativo e Efeitos Colaterais no Vue 3
    </h2>

    <!-- Seção de Parâmetros -->
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <div class="space-y-1">
        <label class="text-xs font-semibold text-slate-600">Salário Base (R$):</label>
        <input
          type="number"
          v-model.number="baseSalary"
          class="w-full border px-3 py-2 rounded-lg text-sm"
        />
      </div>

      <div class="space-y-1">
        <label class="text-xs font-semibold text-slate-600">Score de Performance (0-100):</label>
        <input
          type="range"
          min="0"
          max="100"
          v-model.number="performanceScore"
          class="w-full h-2 bg-slate-200 rounded-lg cursor-pointer"
        />
        <div class="text-right text-xs text-slate-500 font-bold">{{ performanceScore }} pts</div>
      </div>
    </div>

    <!-- Resultados Computados -->
    <div class="p-4 bg-slate-50 rounded-xl border space-y-2">
      <div class="flex justify-between text-sm">
        <span class="text-slate-600">Bônus Calculado (Computed):</span>
        <span class="font-bold text-emerald-600">+{{ (bonusPercentage * 100).toFixed(0) }}%</span>
      </div>
      <div class="flex justify-between text-base font-bold border-t pt-2">
        <span class="text-slate-800">Remuneração Total:</span>
        <span class="text-indigo-600 text-lg">R$ {{ totalCompensation.toFixed(2) }}</span>
      </div>
      <button
        @click="adjustTotalManual"
        class="mt-2 text-xs bg-indigo-50 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-100"
      >
        Forçar Total para R$ 8.000,00 (Testar Writable Computed)
      </button>
    </div>

    <!-- Campo de Busca Assíncrona com Debounce Watcher -->
    <div class="space-y-1">
      <label class="text-xs font-semibold text-slate-600">Pesquisa Assíncrona (Debounced Watch):</label>
      <input
        type="text"
        v-model="searchKeyword"
        placeholder="Digite algo para observar o debounce..."
        class="w-full border px-3 py-2 rounded-lg text-sm"
      />
    </div>

    <!-- Painel de Logs de Auditoria -->
    <div class="space-y-2">
      <h4 class="text-xs font-bold uppercase text-slate-500 tracking-wider">Histórico de Watchers:</h4>
      <div class="bg-slate-900 text-emerald-400 p-3 rounded-lg font-mono text-xs max-h-36 overflow-y-auto space-y-1">
        <p v-if="logs.length === 0" class="text-slate-500">// Nenhum evento disparado ainda...</p>
        <p v-for="(log, idx) in logs" :key="idx">{{ log }}</p>
      </div>
    </div>
  </div>
</template>

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Salário Base: R$ 5.000,00
Performance: 85 pts -> Bônus: +15%
Remuneração Total (Computed): R$ 5.750,00
Console log: "[Audit Sync] Folha recalculada: Base R$ 5000 -> Total R$ 5750"

Interação 1 — Alterar slider de Performance para 95:
Bônus passa automaticamente para +25%.
Remuneração Total atualiza para R$ 6.250,00.
Log adicionado: "[Score Watcher] Alteração de 85 para 95 pontos."

Interação 2 — Clicar em "Forçar Total para R$ 8.000,00":
O setter de totalCompensation calcula o novo salário-base: 8000 / 1.25 = R$ 6.400,00.
baseSalary.value é atualizado para 6400 reativamente.

Interação 3 — Digitar rapidamente "vue" na pesquisa:
O debounce aguarda 500ms de inatividade.
Log adicionado: "[API Search] Consulta disparada para termo: 'vue'".

🔍 Destaques da Implementação

  1. Computed Caching: bonusPercentage só recalcula se performanceScore.value sofrer alteração, retornando o valor em cache em leituras repetidas.
  2. Writable Computed: O padrão get/set viabiliza manipulação bidirecional de dados sem violar a regra de responsabilidade única.
  3. onCleanup Anti-Race-Condition: Garante cancelamento de timeouts e abortamento de requisições de rede ao digitar novos caracteres.