Tutorial VueJS 3 – Comunicar eventos com defineEmits
verificado a 2 Setembro 2026 · 3 min
Um componente filho assinala uma alteração ao pai emitindo um evento: defineEmits declara-o, emit('add-to-cart', id) envia-o e o pai escuta-o com @add-to-cart. Os props descem, os eventos sobem.
Os props descem do pai para o filho. Para fazer subir uma informação, um componente emite um evento e o pai escuta-o, exatamente como escutaria um clique.
O problema
Desde a divisão feita no capítulo 8, o carrinho vive em App.vue e o botão em ProductDisplay.vue. O filho não pode modificar cart: não lhe tem acesso, e um componente não deve escrever no estado de outro.
A formulação certa não é «o filho acrescenta ao carrinho», mas «o filho anuncia que houve um clique, o pai decide o que fazer com isso».
Declarar e emitir
No filho, declara os eventos emitidos e depois emite:
<script setup>
const emit = defineEmits(['add-to-cart'])
function addToCart() {
emit('add-to-cart')
}
</script>
<template>
<button
class="button"
:class="{ disabledButton: !inStock }"
:disabled="!inStock"
@click="addToCart"
>
Ajouter au panier
</button>
</template>O defineEmits é uma macro do compilador, tal como o defineProps: sem import. Devolve a função que serve para emitir.
Declarar os eventos não é obrigatório, mas compensa: a lista documenta o contrato do componente, o editor completa-a automaticamente e o Vue avisa se emitires um nome não declarado.
Escutar do lado do pai
Um evento personalizado escuta-se com a mesma sintaxe de um evento nativo:
<script setup>
import { ref } from 'vue'
import ProductDisplay from './components/ProductDisplay.vue'
const cart = ref(0)
function updateCart() {
cart.value += 1
}
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" @add-to-cart="updateCart" />
</template>Clica: o contador volta a andar. O botão está no filho, o contador no pai, e nenhum dos dois conhece o estado do outro.
Transmitir um dado
Um carrinho realista guarda o que lá pomos, não apenas quantos. Passa o identificador da variante como segundo argumento do emit:
<script setup>
function addToCart() {
emit('add-to-cart', variants.value[selectedVariant.value].id)
}
</script>O pai recebe-o como parâmetro da sua função:
<script setup>
const cart = ref([])
function updateCart(id) {
cart.value.push(id)
}
</script>
<template>
<div class="cart">Panier ({{ cart.length }})</div>
<ProductDisplay :premium="premium" @add-to-cart="updateCart" />
</template>O cart passou a ser um array, por isso a apresentação usa cart.length. Acrescenta várias vezes a T-shirt azul e confirma nas ferramentas de desenvolvimento do Vue que o array se enche mesmo de 2234.
Para várias informações, emite um objeto em vez de uma longa lista de argumentos:
emit('add-to-cart', { id: variant.id, taille: 'M', quantite: 1 })O nome dos eventos
Escreve o nome em kebab-case: add-to-cart, não addToCart. Os atributos HTML não distinguem maiúsculas de minúsculas, um template escrito diretamente numa página receberia @addtocart e o listener nunca chegaria a disparar. Num ficheiro .vue, o Vue corrige a caixa, mas mais vale ganhar já o hábito certo.
Um evento personalizado só se propaga ao pai direto. Se três níveis de componentes separarem o emissor do destinatário, é preciso reemiti-lo em cada andar. Quando isso se torna maçador, é sinal de que precisas de um estado partilhado: é exatamente esse o papel do Pinia.
O sentido de circulação
Uma avaliação que chega por este caminho pode depois ser animada ao aparecer.
Retém estas duas frases: os props descem, os eventos sobem. O pai é dono do dado, o filho pede que ele seja alterado. É isso que torna o fluxo previsível e a origem de uma alteração sempre rastreável.
Os eventos declaram-se na opção emits e emitem-se com this.$emit('add-to-cart', id). O pai escuta-os exatamente da mesma maneira.
remove-from-cart emitido por um segundo botão e faz com que o pai retire do array o último identificador.Erros frequentes
@addtocart e o listener nunca chegaria a disparar. Usa o kebab-case.