Capítulo 05: Comunicação com
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: () => { ... }
});