Vue.js: Comunicação entre Componentes

Última atualização: 2026-10-20

A comunicação entre componentes é um mecanismo central do Vue — os componentes pais passam dados para os componentes filhos por meio de props, e os componentes filhos acionam eventos usando emits para notificar os componentes pais. Compreender o fluxo bidirecional de dados é fundamental para criar boas aplicações Vue.

O <script setup> do Vue 3 oferece duas macros de tempo de compilação, defineProps e defineEmits, que podem ser usadas sem importação e oferecem suporte total à inferência de tipos do TypeScript.

Template e Auxiliares relacionados:useAttrs() v-model

1. O que você vai aprender


2. A armadilha da “ligação bidirecional” no carrinho de compras de um site de comércio eletrônico

(1) Problema: Quando um componente filho altera uma propriedade, o componente pai gera um erro

Alice implementou um carrinho de compras com um campo para inserção da quantidade:

O Vue gera um erro:

[Vue warn] Set operation on key "quantity" failed: target is readonly.

Após uma hora de depuração, Charlie descobriu que:

“Alice, o total do carrinho do pai não é atualizado quando altero a quantidade. O Vue está alterando a propriedade, mas o pai não percebe.”

(2) Fluxo de dados unidirecional no Vue + Solução emit

Agora as alterações fluem corretamente: o filho emite → o pai recebe → o pai atualiza os dados → repassa ao filho.

(3) Receita

Após corrigir o fluxo de dados:


3. Sintaxe completa para props

(1) Declarações para 7 tipos de adereços

VUE
<script setup>
defineProps({
  // 1. Basic Types
  name: String,
  age: Number,
  active: Boolean,
  
  // 2. Complex Types
  user: Object,
  items: Array,
  
  // 3. Multiple Types(Any)
  id: [String, Number],
  
  // 4. Required
  title: { type: String, required: true },
  
  // 5. Default value
  pageSize: { type: Number, default: 20 },
  
  // 6. Custom Validation
  email: {
    type: String,
    validator: (val) => val.includes('@')
  },
  
  // 7. Function Default Values(The default value must be a function.)|
  createdAt: {
    type: Date,
    default: () => new Date()
  }
})
</script>

(2) Exemplo detalhado

VUE
<script setup>
defineProps({
  // String
  タイトル: String,
  
  // Numbers + Default value + Verification
  pageSize: {
    type: Number,
    default: 20,
    validator: (val) => val > 0 && val <= 100
  },
  
  // Boolean(Note:The default value for falsy Values should be in function form)
  isVip: {
    type: Boolean,
    default: false
  },
  
  // Array Default Values(It must be returned as a function)
  tags: {
    type: Array,
    default: () => ['vue', 'javascript']
  },
  
  // Object Default Values(It must be returned as a function)
  user: {
    type: Object,
    default: () => ({ name: 'Guest', age: 0 })
  },
  
  // Required + Custom Validation
  email: {
    type: String,
    required: true,
    validator: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
  }
})
</script>
▶ Experimente

(3) 6 Regras de valor padrão

Regra Exemplo
Tipos básicos default: 20
Matrizes/Objetos default: () => []
null default: null
indefinido Não é necessário definir um valor padrão
Referência de funções Não use default: fn; use () => fn()
Símbolo / BigInt A função de fábrica retorna

4. Props com tipagem forte no TypeScript (recomendado para o Vue 3)

(1) Declarações de tipos primitivos

VUE
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

defineProps<{
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}>()
</script>

(2) Obrigatório x Opcional (?)

TS
defineProps<{
  // Required(Default)
  user: User
  pageSize: number
  
  // Optional (add ?)
  variant?: 'primary' | 'secondary'
  showIcon?: boolean
}>()

(3) withDefaults fornece valores padrão

VUE
<script setup lang="ts">
interface Props {
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  showEmail: true
})
</script>

(4) Comparação entre declarações em tempo de execução e declarações de tipo

Dimensão Tempo de execução defineProps({}) Tipo defineProps<T>()
Inferência de tipos Fraca Forte (autocompletar do IDE)
Valor padrão Padrão direto wrapper withDefaults
Validação função de validação verificação em tempo de compilação do TypeScript
Recomendado Projetos pequenos/JS puro Projetos grandes/projetos em TS

5. Sintaxe completa para emit

(1) 5 tipos de declarações de evento

VUE
<script setup>
// 1. A Brief Statement
const emit = defineEmits(['click', 'submit', 'cancel'])

// 2. Parameter Validation
const emit = defineEmits({
  addToCart: (productId) => typeof productId === 'number',
  remove: (id) => typeof id === 'string'
})

// 3. TypeScript Type
const emit = defineEmits<{
  'add-to-cart': [productId: number]
  'remove-item': [id: string, reason: string]
  'update': [id: number, data: object]
}>()
</script>

(2) 5 métodos de acionamento

VUE
<script setup>
const emit = defineEmits(['click', 'submit', 'add-to-cart'])

// 1. Simple Trigger
emit('click')

// 2. Passing Parameters
emit('add-to-cart', 123)

// 3. Pass multiple parameters
emit('submit', { name: 'Alice' }, 2026)

// 4. Conditional Trigger
if (isValid) emit('submit')

// 5. Functional Triggering
function handleAdd() {
  emit('add-to-cart', props.product.id)
}
</script>

(3) Escuta do componente pai

(4) Validação de tipo para emit

VUE
<script setup>
const emit = defineEmits({
  // ✅ Verification:id It must be number
  remove: (id) => typeof id === 'number' || 'Validation failed',
  
  // ❌ Throws Error: When parameters do not match
  // remove: 'invalid',  // Wrong: It must be a function
})
</script>

▶ Exemplo: Componente pai utilizando ProductCard

VUE
<!-- src/views/ProductListView.vue -->
<template>
  <div class="grid">
    <ProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      :show-stock-badge="true"
      @add-to-cart="handleAddToCart"
      @quick-view="handleQuickView"
      @toggle-favorite="handleToggleFavorite"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useCart } from '@/composables/useCart'

const products = ref([
  { id: 1, name: 'iPhone', price: 999, stock: 50, image: 'iphone.jpg' },
  { id: 2, name: 'MacBook', price: 2499, stock: 0, image: 'macbook.jpg' }
])

const { addToCart } = useCart()

function handleAddToCart(productId) {
  addToCart(productId)
}
function handleQuickView(productId) {
  console.log('Quick view:', productId)
}
function handleToggleFavorite(productId, isFavorite) {
  console.log('Toggle:', productId, isFavorite)
}
</script>
▶ Experimente

Saída:

TEXT
Grade com iPhone (botão ativo) e MacBook "Out of Stock"
Clique no iPhone → handleAddToCart(1) → addToCart(1)
@quick-view / @toggle-favorite → console: Quick view: 1 / Toggle: 1 false

6. O princípio do fluxo unidirecional de dados

(1) Princípios Fundamentais

100%
graph LR
    A[Parent Component data] -->|props| B[Child component]
    B -->|emit| A
    
    style A fill:#42b883
    style B fill:#42b883

(2) 5 Exceções

VUE
<script setup>
const props = defineProps({ user: Object })

// ❌ Wrong: Edit prop directly
props.user.name = 'Bob'  // Vue Warning!

// ✅ Exception 1:prop When it is a reference type,Can be replaced with a new object
// The parent component passes a reference,Child components cannot change their references,But it's possible emit Let Father Change It

// ✅ Exception 2:prop Initial values can be saved locally
const localUser = ref({ ...props.user })
// Then make the changes localUser(Does not affect the parent)

// ✅ Exception 3: Use computed Derivative
const userName = computed(() => props.user.name)

// ✅ Exception 4: Use v-model (Syntax Sugar)
// <Child v-model="value" /> equivalent to :value + @update:value

// ✅ Exception 5:provide/inject Across levels
// Parent provide, Child inject (Learned in Phase 2.5)
</script>

(3) 5 antipadrões

JS
// ❌ Anti-pattern 1:Edit directly prop
props.user.name = 'Bob'

// ❌ Anti-pattern 2:Use in child components watch Edit prop
watch(() => props.value, (val) => { props.value = val * 2 })

// ❌ Anti-pattern 3: Two-way bound ref passthrough (Not recommended)
const localRef = ref(props.value)
watch(localRef, (val) => emit('update', val))

// ❌ Anti-pattern 4:Usage v-model Does not follow the naming convention
// v-model Expectations update:xxx Event,Custom event names must be consistent
emit('change', val)  // ❌ Parent Component @change Will not be triggered
emit('update:value', val)  // ✅ v-model Supporting

// ❌ Anti-pattern 5:Managing the parent component's data directly within a child component
emit('update', { ...props.user, name: 'Bob' })  // Wrong: Should only pass ID, Parent updates itselff

▶ Exemplo: Vinculação bidirecional com v-model

VUE
<!-- Child component CustomInput.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- Using Parent Components v-model -->
<template>
  <CustomInput v-model="searchQuery" />
  <p>Query: {{ searchQuery }}</p>
</template>

<script setup>
import { ref } from 'vue'
import CustomInput from '@/components/CustomInput.vue'
const searchQuery = ref('')
</script>
▶ Experimente

Saída:

TEXT
input vazio; Query: (vazio)
Digite "vue" → Query: vue (update:modelValue a cada tecla)

7. Passagem de propriedades: inheritAttrs e useAttrs

(1) Comportamento padrão

(2) Desativar o repasse automático

(3) Uso explícito de useAttrs

(4) 5 Casos de uso

Cenário Método
Padrão Não é necessária nenhuma ação; a classe é aplicada automaticamente à raiz
Elemento raiz personalizado useAttrs() Vinculação explícita
Propagar para elementos que não sejam raiz inheritAttrs: false + v-bind="$attrs"
Vários componentes (Fragmentos) É necessário usar inheritAttrs: false + ligação explícita
Otimização de desempenho Desativar o repasse para evitar atributos DOM desnecessários

▶ Exemplo: useAttrs Pass-Through

▶ Experimente

Saída:

TEXT
<label>Name</label>
<input placeholder="Enter name..." class="input-field">
Atributos extras vão para o input, não para o label

8. Exercício abrangente: ProductCard com props e emits completos

▶ Exemplo: ProductCard.vue — Props e emits completos

▶ Experimente

Saída:

TEXT
Card: "iPhone", "$999", botão "Add to Cart"
Clique → emit('add-to-cart', 1)
stock = 0 → "Out of Stock", botão desabilitado, card com opacity

(1) 📊 5. Referência rápida para 5 erros comuns

Erro Sintoma Solução
Modificar a propriedade diretamente Aviso do Vue Use emit para permitir que o pai a modifique
Matrizes/objetos: as propriedades têm valores padrão como referências Compartilhadas por vários componentes default: () => [] Fábrica
Incompatibilidade no nome do evento do v-model O pai não o recebe Use update:xxx
o validador retorna uma string o Vue não gera um erro retorna true/false
Repasse para vários componentes Aviso do Vue inheritAttrs: false

(2) 📊 6. Cinco principais cenários de validação para props

Cenário Verificação
E-mail val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
URL val => /^https?:\/\//.test(val)
Valor da enumeração val => ['a', 'b', 'c'].includes(val)
Gama val => val >= 0 && val <= 100
Objeto personalizado val => val.id && val.name

Template e Auxiliares relacionados:useAttrs() v-model

❓ Perguntas Frequentes

P É possível modificar props diretamente?
R Não. De acordo com o princípio do fluxo de dados unidirecional do Vue, os props são somente de leitura. Para modificar os dados, um componente filho deve emitir um evento para que o componente pai possa realizar a alteração. Modificar props diretamente acionará um aviso do Vue.
P Como faço para passar dados complexos (objetos aninhados)?
R Passe a referência diretamente; o Vue lida automaticamente com o binding profundo. Quando você modifica uma propriedade dentro do objeto, o componente pai detecta a alteração (binding profundo), mas os componentes filhos não devem modificá-la diretamente; em vez disso, devem usar emit para permitir que o componente pai atualize o objeto ou a propriedade inteira.
P Qual é a diferença entre as opções defineProps e props?
R No <script setup>, use a macro defineProps() (não é necessária importação); na API de Opções, use a opção props: {}. As duas são equivalentes; recomendamos usar setup + defineProps.
P O emit pode ser acionado de forma assíncrona em um componente filho?
R Sim. O emit é uma chamada de função síncrona, mas pode ser encapsulado em setTimeout ou em um Promise: setTimeout(() => emit('done'), 1000).
P Qual é a essência de v-model?
R Trata-se de um sintaxe simplificada para props e emit. <Child v-model="x" /> é equivalente a :modelValue="x" @update:modelValue="x = $event". Consulte o Exemplo 3 desta lição para obter uma explicação detalhada.
P Como faço para monitorar alterações nas propriedades de um componente filho?
R Use watch(() => props.xxx, (newVal, oldVal) => {...}). Tome cuidado para não modificar as propriedades dentro do watch, ou você entrará em um loop infinito.
P Quando se deve usar useAttrs?
R Em três situações: (1) É necessário para componentes com múltiplas raízes (Fragments); (2) Quando se deseja passar atributos para elementos que não são raiz; (3) Quando se deseja controlar explicitamente onde os atributos são vinculados. Não é necessário para componentes comuns com uma única raiz.
P O que acontece se a validação dos props falhar?
R No ambiente de desenvolvimento, o Vue exibe um aviso; no ambiente de produção, a validação falha silenciosamente. Recomenda-se que o validador retorne false (erro) ou uma string (mensagem de erro).

📖 Resumo


📝 Exercícios

  1. Questões básicas (Dificuldade: ⭐)

    Implemente um componente Button simples:

    • parâmetros: text (String), variant (String, 'primário'/'sucesso'/'perigo'), disabled (Booleano)
    • As 3 variantes correspondem a 3 classes CSS
    • emitir o evento click (sem parâmetros)
    • Teste as três variantes no componente pai
  2. Problemas avançados (Dificuldade: ⭐⭐)

    Implemente um componente de entrada com vinculação bidirecional:

    • Child CustomInput.vue: props: modelValue, label
    • Usando o “sugar sintático” v-model
    • emitir update:modelValue
    • Componente pai: exibe as entradas do usuário em tempo real
  3. Problema de desafio (Dificuldade: ⭐⭐⭐)

    Implementar um sistema abrangente de “Ficha do produto + Favoritos”:

    1. ProductCard.vue: Props completas (product/showStockBadge/maxStock) + emissões (add-to-cart/toggle-favorite)
    2. App.vue: Lista de produtos + Status dos favoritos
    3. Botão “Favorito”: emite o evento toggle-favorite; o componente pai atualiza o status de favorito
    4. Implementar componentes com múltiplas raízes usando useAttrs e inheritAttrs (sem comprometer o exercício)
    5. Verificado por pelo menos 3 validadores (e-mail/URL/enum)
    6. Props fortemente tipadas do TypeScript
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%