Props y Emits en Vue 3: comunicación entre componentes
Cuando una aplicación empieza a dividirse en componentes, aparece una pregunta muy simple: ¿cómo se pasan información entre ellos?
En Vue, la respuesta básica suele ser esta:
- Las
propspasan datos desde el componente padre hacia el componente hijo. - Los
emitspermiten que el componente hijo avise algo al componente padre.
Ese flujo puede parecer pequeño, pero es una de las ideas que más orden le da a una interfaz. El padre conserva el estado importante. El hijo recibe lo que necesita para mostrarse y emite eventos cuando pasa algo que el padre debería conocer.
El flujo normal: datos bajan, eventos suben
Imagina una lista de tareas. El componente padre tiene el arreglo completo:
<script setup>
import { ref } from 'vue'
import TaskCard from './TaskCard.vue'
const tasks = ref([
{ id: 1, title: 'Leer la guía de props', completed: false },
{ id: 2, title: 'Crear un componente pequeño', completed: false }
])
const completeTask = (taskId) => {
const task = tasks.value.find((item) => item.id === taskId)
if (task) {
task.completed = true
}
}
</script>
<template>
<TaskCard
v-for="task in tasks"
:key="task.id"
:task="task"
@complete="completeTask"
/>
</template>El padre conoce la lista, sabe cómo actualizarla y decide qué hacer cuando una tarea cambia.
El componente hijo, en cambio, solo necesita mostrar una tarea y avisar cuando el usuario hace clic:
<script setup>
const props = defineProps({
task: {
type: Object,
required: true
}
})
const emit = defineEmits(['complete'])
</script>
<template>
<article>
<h2>{{ props.task.title }}</h2>
<p>{{ props.task.completed ? 'Completada' : 'Pendiente' }}</p>
<button
type="button"
:disabled="props.task.completed"
@click="emit('complete', props.task.id)"
>
Marcar como completada
</button>
</article>
</template>Este patrón mantiene una separación sana: el hijo no necesita saber cómo está guardada la lista completa. Solo dice: "esta tarea debería completarse".
defineProps: qué necesita recibir el componente
Con <script setup>, las props se declaran con defineProps().
<script setup>
const props = defineProps({
name: {
type: String,
required: true
},
role: {
type: String,
default: 'Miembro'
}
})
</script>
<template>
<div>
<strong>{{ props.name }}</strong>
<span>{{ props.role }}</span>
</div>
</template><script>
export default {
props: {
name: {
type: String,
required: true
},
role: {
type: String,
default: 'Miembro'
}
}
}
</script>
<template>
<div>
<strong>{{ name }}</strong>
<span>{{ role }}</span>
</div>
</template>Una prop debería responder una pregunta concreta: ¿qué dato necesita este componente para poder renderizarse?
Si el componente necesita un nombre, una URL, un estado activo o una lista pequeña, eso probablemente es una prop. Si necesita modificar estado compartido, guardar datos o coordinar muchas piezas, quizá ya no estamos hablando de una simple prop.
defineEmits: qué puede comunicar el componente
Los emits describen eventos que el componente hijo puede disparar.
<script setup>
const emit = defineEmits(['search'])
const handleSubmit = (event) => {
const form = event.currentTarget
const query = new FormData(form).get('query')
emit('search', String(query || '').trim())
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input name="query" type="search">
<button type="submit">Buscar</button>
</form>
</template><script>
export default {
name: 'SearchBox',
emits: ['search'],
methods: {
handleSubmit(event) {
const form = event.currentTarget
const query = new FormData(form).get('query')
this.$emit('search', String(query || '').trim())
}
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input name="query" type="search">
<button type="submit">Buscar</button>
</form>
</template>El componente no decide qué hacer con la búsqueda. Puede que el padre llame una API, actualice la URL o filtre una lista local. El hijo solo comunica el evento con los datos necesarios.
No modifiques props directamente
Una regla importante: un componente hijo no debería modificar una prop recibida.
Esto es tentador:
<script setup>
const props = defineProps({
task: {
type: Object,
required: true
}
})
const complete = () => {
props.task.completed = true
}
</script>Aunque con objetos puede parecer que funciona, el diseño se vuelve confuso. El padre deja de ser el lugar claro donde cambia el estado y el hijo empieza a tener demasiado control sobre algo que no le pertenece.
Una versión más limpia es emitir la intención:
<script setup>
const props = defineProps({
task: {
type: Object,
required: true
}
})
const emit = defineEmits(['complete'])
const complete = () => {
emit('complete', props.task.id)
}
</script><script>
export default {
name: 'TaskCard',
props: {
task: {
type: Object,
required: true
}
},
emits: ['complete'],
methods: {
complete() {
this.$emit('complete', this.task.id)
}
}
}
</script>
<template>
<article>
<h2>{{ task.title }}</h2>
<p>{{ task.completed ? 'Completada' : 'Pendiente' }}</p>
<button type="button" @click="complete">
Marcar como completada
</button>
</article>
</template>El padre recibe el evento y hace el cambio:
<TaskCard
:task="task"
@complete="completeTask"
/>Nombrar eventos con intención
Un buen nombre de evento dice qué ocurrió, no cómo debería reaccionar el padre.
Por ejemplo:
@submit@select@remove@complete@update:status
Evita nombres demasiado acoplados como @callApiNow o @changeParentList. El hijo no debería saber tanto sobre lo que hará el padre.
Props pequeñas, eventos claros
El problema no suele ser usar props. El problema aparece cuando un componente recibe demasiadas props sueltas y emite eventos difíciles de seguir.
Si ves algo así:
<UserCard
:id="user.id"
:name="user.name"
:email="user.email"
:avatar="user.avatar"
:role="user.role"
:is-active="user.isActive"
:last-login="user.lastLogin"
/>tal vez el componente debería recibir un objeto user completo:
<UserCard :user="user" />No siempre. Si el componente solo necesita dos campos, pasar props específicas puede ser más explícito. Pero cuando todas las props representan la misma entidad, agruparlas puede hacer el template más claro.
Cuándo usar props y emits
Props y emits son ideales cuando:
- Hay una relación directa entre padre e hijo.
- El estado vive naturalmente en el padre.
- El hijo solo necesita mostrar datos y avisar interacciones.
- Quieres que el flujo sea fácil de leer desde el template.
Si empiezas a pasar la misma prop por cinco niveles, probablemente necesitas otra herramienta. Ahí puede tener sentido mirar provide/inject, un composable compartido o una solución de estado como Pinia.
Pero no saltes a esas opciones demasiado pronto. Para muchísimos componentes, props y emits son suficientes.
Una forma simple de pensarlo
Antes de conectar dos componentes, pregúntate:
¿Este componente necesita recibir un dato o necesita avisar que algo ocurrió?
Si necesita recibir un dato, usa una prop.
Si necesita avisar algo, usa un emit.
Esa distinción mantiene el código predecible. Los datos bajan, los eventos suben, y cada componente conserva una responsabilidad clara.
