Capítulo 9 de 11

Tutorial de VueJS 3 – Comunicación de eventos con emit

verificado el 2 septiembre 2026 · 3 min

Respuesta rápida

Un componente hijo avisa al padre de un cambio emitiendo un evento: defineEmits lo declara, emit('add-to-cart', id) lo envía y el padre lo escucha con @add-to-cart. Las props bajan, los eventos suben.

Al final de este capítulo, sabrás hacer subir una información de un componente hacia su padre.

Las props bajan del padre al hijo. Para hacer subir una información, un componente emite un evento y el padre lo escucha, exactamente igual que escucharía un clic.

El problema

Desde la división del capítulo 8, el carrito vive en App.vue y el botón en ProductDisplay.vue. El hijo no puede modificar cart: no tiene acceso, y un componente no debe escribir en el estado de otro.

La formulación correcta no es «el hijo añade al carrito», sino «el hijo anuncia que se ha hecho clic y el padre decide qué hacer con ello».

Declarar y emitir

En el hijo, declara los eventos que emite y luego emítelos:

src/components/ProductDisplay.vue
<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>

defineEmits es una macro del compilador, como defineProps: no hay que importar nada. Devuelve la función que sirve para emitir.

Declarar los eventos no es obligatorio, pero viene bien: la lista documenta el contrato del componente, el editor la autocompleta y Vue avisa si emites un nombre que no has declarado.

Escuchar en el padre

Un evento personalizado se escucha con la misma sintaxis que un evento nativo:

src/App.vue
<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>

Haz clic: el contador vuelve a moverse. El botón está en el hijo, el contador en el padre, y ninguno de los dos conoce el estado del otro.

Pasar un dato

Un carrito realista recuerda lo que se mete en él, no solo cuánto. Pasa el identificador de la variante como segundo argumento de emit:

src/components/ProductDisplay.vue
<script setup>
function addToCart() {
  emit('add-to-cart', variants.value[selectedVariant.value].id)
}
</script>

El padre lo recibe como parámetro de su función:

src/App.vue
<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>

cart ha pasado a ser un array, así que la vista usa cart.length. Añade varias veces la camiseta azul y comprueba en las herramientas de desarrollo de Vue que el array se va llenando de 2234.

Si son varias informaciones, emite un objeto en lugar de una larga lista de argumentos:

javascript
emit('add-to-cart', { id: variant.id, taille: 'M', quantite: 1 })

El nombre de los eventos

Escribe el nombre en kebab-case: add-to-cart, no addToCart. Los atributos HTML no distinguen mayúsculas, una plantilla escrita directamente en una página recibiría @addtocart y el listener no se dispararía nunca. En un archivo .vue, Vue corrige la caja, pero mejor coger la buena costumbre.

Los eventos no suben solos

Un evento personalizado solo se propaga al padre directo. Si tres niveles de componentes separan al emisor del destinatario, hay que volver a emitirlo en cada piso. Cuando eso empieza a molestar, es la señal de que hace falta un estado compartido: exactamente el papel de Pinia.

El sentido de la circulación

Una reseña que llega por este camino se puede después animar al mostrarla.

Quédate con estas dos frases: las props bajan, los eventos suben. El padre es dueño del dato, el hijo pide que se modifique. Eso es lo que hace el flujo predecible y mantiene siempre rastreable el origen de un cambio.

Con la Options API

Los eventos se declaran en la opción emits y se emiten con this.$emit('add-to-cart', id). El padre los escucha exactamente igual.

EjercicioAñade un evento remove-from-cart emitido por un segundo botón, y haz que el padre quite del array el último identificador.

Errores frecuentes

Nombrar el evento en camelCase Los atributos HTML ignoran la caja: una plantilla fuera de un archivo .vue recibiría @addtocart y el listener no se dispararía nunca. Usa kebab-case.
Esperar que un evento atraviese varios niveles Un evento solo llega al padre directo. Más allá de dos niveles, pasa a un estado compartido con Pinia.
Modificar el estado del padre desde el hijo El hijo avisa, el padre decide. Es lo que mantiene rastreable el origen de los cambios.
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.