Capítulo 05: Comunicação com

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


🗺️ Mapa Conceitual da Comunicação de Componentes

flowchart TD
    A["Componente Pai (Ancestral)"] -->|"1. Entrada de Dados: defineProps"| B["Componente Filho (<script setup>)"]
    B -->|"2. Emissão de Eventos: defineEmits"| A
    A <-->|"3. Two-Way Binding Moderno: defineModel (Vue 3.4+)"| B
    B -.->|"4. Exposição Seletiva de Métodos: defineExpose"| A

    subgraph MACROS["Compiler Macros (Tempo de Compilação)"]
        C["defineProps<Props>()"]
        D["withDefaults()"]
        E["defineEmits<{ (e: 'event'): void }>()"]
        F["defineModel<T>()"]
        G["defineSlots<Slots>()"]
    end

    B --- MACROS

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style MACROS fill:#ede7f6,stroke:#7e57c2,stroke-width:2px

🏛️ 1. Macros de Compilação e Comunicação entre Componentes

No Vue 3 com <script setup>, utilitários como defineProps e defineEmits não são funções JavaScript convencionais importadas em tempo de execução; são Compiler Macros avaliadas pelo @vue/compiler-sfc durante a transformação do código para funções de renderização. Elas não precisam ser importadas manualmente via import { ... } from 'vue'.

1.1 defineProps com TypeScript e withDefaults

A abordagem recomendada é a declaração puramente genérica através de interfaces TypeScript:

interface CardProps {
  title: string;
  count?: number;
  variant?: 'primary' | 'danger';
}

const props = withDefaults(defineProps<CardProps>(), {
  count: 0,
  variant: 'primary'
});
  • Vantagem: Elimina a duplicação entre interfaces estáticas do TypeScript e o objeto de validação de runtime do Vue (type: String, required: true). O compilador sintetiza a validação de runtime automaticamente a partir da interface.

1.2 defineEmits e Contratos Estritos de Eventos

Permite declarar exatamente quais eventos o componente pode emitir e a assinatura de seus payloads:

const emit = defineEmits<{
  (e: 'updateStatus', newStatus: 'ACTIVE' | 'ARCHIVED'): void;
  (e: 'close'): void;
}>();

O TypeScript acusará erro de compilação imediato caso um evento seja emitido com argumentos divergentes do contrato.

1.3 A Revolução do defineModel() (Vue 3.4+)

Historicamente, implementar v-model exigia declarar uma prop modelValue e disparar o evento update:modelValue. No Vue 3.4+, a macro defineModel() sintetiza essa vinculação bidirecional com uma única linha:

// Componente Filho:
const model = defineModel<string>({ default: '' });
// Mutações em model.value atualizam instantaneamente o estado no componente Pai!

⚖️ 2. Tabela Comparativa de Estratégias de Comunicação

Mecanismo Direção do Fluxo Acoplamento Caso de Uso Primário
defineProps Pai ➔ Filho Baixo (Unidirecional). Envio de configurações, títulos, dados brutos e flags visuais.
defineEmits Filho ➔ Pai Baixo (Inversão de controle). Notificação de ações do usuário (cliques, confirmações, fechamentos).
defineModel Bidirecional (Pai 🔁 Filho) Baixo (Two-Way Binding padronizado). Inputs customizados, formulários, sliders, toggles de tema.
defineExpose Filho ➔ Pai (via Template Ref) Alto (Imperativo). Controle de nós de canvas, reprodutores de vídeo ou animações manuais.

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

Anti-Pattern 1: Mutação Direta de Props no Componente Filho

Erro Comum: Escrever props.user.name = 'Novo Nome' diretamente dentro do componente filho. Isso viola o princípio do Fluxo Unidirecional de Dados (Unidirectional Data Flow), provocando efeitos colaterais imprevisíveis e advertências no console do Vue. Abordagem Correta: O filho deve emitir um evento (emit('updateName', novoNome)) ou utilizar defineModel().

Anti-Pattern 2: Importar defineProps ou defineEmits

Erro Comum: import { defineProps, defineEmits } from 'vue';. Como são macros de compilação, o import é desnecessário e pode gerar avisos em ferramentas de lint.


💻 4. Código Conceitual Arquitetural Comentado

<!-- src/components/SegmentedSwitch.vue -->
<script setup lang="ts">
import { computed } from 'vue';

// 1. defineModel para Two-Way Data Binding tipado nativo
const activeSegment = defineModel<string>({ required: true });

// 2. defineProps com interface TypeScript estrita
interface SegmentOption {
  value: string;
  label: string;
  badge?: number;
}

interface SwitchProps {
  options: SegmentOption[];
  disabled?: boolean;
}

const props = withDefaults(defineProps<SwitchProps>(), {
  disabled: false
});

// 3. defineEmits tipado para notificação de telemetria
const emit = defineEmits<{
  (e: 'segmentChanged', payload: { previous: string; current: string }): void;
}>();

function selectSegment(val: string): void {
  if (props.disabled || activeSegment.value === val) return;

  const previous = activeSegment.value;
  activeSegment.value = val; // Dispara atualização automática no componente pai
  emit('segmentChanged', { previous, current: val });
}
</script>

<template>
  <div class="inline-flex p-1 bg-slate-100 rounded-xl border border-slate-200" :class="{ 'opacity-50 pointer-events-none': props.disabled }">
    <button
      v-for="opt in props.options"
      :key="opt.value"
      type="button"
      @click="selectSegment(opt.value)"
      class="px-4 py-1.5 rounded-lg text-xs font-semibold transition-all flex items-center gap-1.5"
      :class="activeSegment === opt.value ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-800'"
    >
      <span>{{ opt.label }}</span>
      <span
        v-if="opt.badge !== undefined"
        class="px-1.5 py-0.2 rounded-full text-[10px]"
        :class="activeSegment === opt.value ? 'bg-indigo-100 text-indigo-700' : 'bg-slate-200 text-slate-600'"
      >
        {{ opt.badge }}
      </span>
    </button>
  </div>
</template>

🛠️ 5. Cheat-Sheet das Compiler Macros

// Declaração de Props com Defaults
interface Props { id: string; active?: boolean }
const props = withDefaults(defineProps<Props>(), { active: true });

// Declaração de Emits Tipados
const emit = defineEmits<{
  (e: 'save', id: string): void;
  (e: 'cancel'): void;
}>();

// Two-Way Binding Simplificado (Vue 3.4+)
const modelo = defineModel<string>();
const modeloObrigatorio = defineModel<number>({ required: true });

// Exposição Controlada para Template Refs do Pai
defineExpose({
  metodoPublico: () => { ... }
});