En esta página
Props, emits y defineModel()
Comunicación entre componentes en Vue
En Vue, los datos fluyen de una manera específica: de padre a hijo mediante props y de hijo a padre mediante emits. Este flujo unidireccional hace que la aplicación sea predecible y fácil de depurar.
Padre ──props──► Hijo
Padre ◄──emit── HijoPara datos que necesitan compartirse entre componentes sin relación directa (hermanos, componentes distantes), se usa provide/inject o Pinia.
defineProps\() — declarar props con TypeScript
La forma recomendada de declarar props es con la sintaxis de genérico de TypeScript:
<script setup lang="ts">
const props = defineProps<{
titulo: string // Requerido
descripcion?: string // Opcional (undefined si no se pasa)
cantidad: number
activo?: boolean
variante?: 'primary' | 'secondary' | 'danger'
tags?: string[]
}>()
// Acceso: props.titulo, props.cantidad, etc.
console.log(props.titulo)
</script>withDefaults() para valores por defecto
<script setup lang="ts">
const { variante = 'primary', activo = true, tags = [] } = withDefaults(
defineProps<{
titulo: string
variante?: 'primary' | 'secondary' | 'danger'
activo?: boolean
tags?: string[]
}>(),
{
variante: 'primary',
activo: true,
tags: () => [], // Función factory para arrays y objetos
}
)
</script>Vue 3.5 — Reactive Props Destructure
Vue 3.5 permite desestructurar directamente con valores por defecto, y el compilador mantiene la reactividad:
<script setup lang="ts">
import { computed } from 'vue'
// Desestructuración con valores por defecto — reactiva en Vue 3.5
const { nombre, nivel = 1, activo = false } = defineProps<{
nombre: string
nivel?: number
activo?: boolean
}>()
// 'nombre', 'nivel', 'activo' son reactivos — el computed funciona
const etiqueta = computed(() => `[${nivel}] ${nombre}`)
</script>El compilador de Vue 3.5 transforma automáticamente los accesos a las variables desestructuradas en props.nombre, props.nivel, etc., manteniendo el rastreo reactivo.
defineEmits\() — declarar eventos tipados
Los emits declaran qué eventos puede emitir un componente hacia su padre:
<script setup lang="ts">
// Sintaxis de tupla tipada (Vue 3.3+)
const emit = defineEmits<{
click: [] // Sin argumentos
cambio: [valor: string] // Un argumento
seleccion: [id: number, nombre: string] // Múltiples argumentos
'actualizar:valor': [valor: string] // Evento con :
}>()
// Uso
function manejarClic(): void {
emit('click')
}
function actualizar(valor: string): void {
emit('cambio', valor)
emit('actualizar:valor', valor)
}
</script>En el componente padre, se escuchan con @nombre-evento:
<template>
<MiComponente
@click="manejarClic"
@cambio="manejarCambio"
@seleccion="(id, nombre) => console.log(id, nombre)"
/>
</template>v-model en componentes — el enfoque clásico
Antes de Vue 3.4, implementar v-model en componentes requería declarar manualmente el prop modelValue y emitir update:modelValue:
<!-- Componente hijo — enfoque clásico -->
<script setup lang="ts">
defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [valor: string] }>()
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template><!-- Componente padre -->
<template>
<MiInput v-model="texto" />
<!-- Equivale a: :modelValue="texto" @update:modelValue="texto = $event" -->
</template>defineModel() — la nueva API (Vue 3.4+)
defineModel() es la forma moderna y concisa de implementar v-model:
<script setup lang="ts">
// defineModel() declara automáticamente el prop y el emit
const modelo = defineModel<string>({ required: true })
// modelo.value es el valor actual
// Asignar a modelo.value emite el evento automáticamente
</script>
<template>
<input v-model="modelo" />
<!-- o -->
<input
:value="modelo"
@input="modelo = ($event.target as HTMLInputElement).value"
/>
</template>Múltiples v-model en un componente
Vue soporta múltiples v-model con nombres personalizados:
<!-- Componente hijo -->
<script setup lang="ts">
const nombre = defineModel<string>('nombre', { required: true })
const apellido = defineModel<string>('apellido', { default: '' })
</script><!-- Componente padre -->
<template>
<EditorNombre
v-model:nombre="usuario.nombre"
v-model:apellido="usuario.apellido"
/>
</template>Modificadores en defineModel()
Puedes acceder a los modificadores de v-model desde el componente:
<script setup lang="ts">
const [modelo, modificadores] = defineModel<string>()
// Si el padre usa v-model.trim, modificadores.trim === true
function actualizarValor(evento: Event): void {
const valor = (evento.target as HTMLInputElement).value
modelo.value = modificadores.trim ? valor.trim() : valor
}
</script>Props booleanas — atajo de sintaxis
Vue tiene un comportamiento especial para props booleanas. Si el nombre del prop es el mismo que el atributo booleano HTML, Vue convierte el atributo a true sin necesidad de especificar el valor:
<!-- Si Boton tiene: disabled?: boolean -->
<Boton disabled />
<!-- equivale a -->
<Boton :disabled="true" />Validación de props en runtime
Además de TypeScript, Vue puede validar props en tiempo de ejecución usando la sintaxis de objeto (útil para librerías):
const props = defineProps({
edad: {
type: Number,
required: true,
validator: (valor: number) => valor >= 0 && valor <= 120,
},
estado: {
type: String as PropType<'activo' | 'inactivo'>,
default: 'activo',
}
})Patrón de componente controlado
Un componente controlado es aquel cuyo estado es completamente controlado por el padre a través de props:
<!-- Controlado — el padre gestiona el estado -->
<script setup lang="ts">
const { valor, activo } = defineProps<{ valor: string; activo: boolean }>()
const emit = defineEmits<{ 'update:valor': [string]; 'update:activo': [boolean] }>()
</script>Vs componente no controlado (gestiona su propio estado interno):
<!-- No controlado — gestiona su propio estado -->
<script setup lang="ts">
import { ref } from 'vue'
const valor = ref('')
const activo = ref(false)
</script>defineModel() crea componentes controlados de forma elegante.
Práctica
- Componente de rating: Crea un componente
Valoracion.vueque muestre 5 estrellas. UsadefineModel<number>()para el valor seleccionado. En el padre, muestra el valor actual y permite cambiarlo. - Formulario controlado: Crea un formulario de contacto donde cada campo es un componente
InputPersonalizado.vuecondefineModel<string>(). El padre gestiona todos los valores y los envía en un objeto al submit. - Múltiples v-model: Crea un componente
RangoFechas.vueconv-model:inicioyv-model:finpara seleccionar un rango de fechas. El padre muestra los valores seleccionados.
En la siguiente lección aprenderemos a reutilizar lógica con composables, el patrón más poderoso de Vue 3.
<script setup lang="ts">
// defineModel() — Vue 3.4+ — para v-model en componentes
const modelo = defineModel<string>({ required: true })
// Props adicionales
const {
etiqueta = 'Campo',
placeholder = '',
tipo = 'text',
error = '',
} = defineProps<{
etiqueta?: string
placeholder?: string
tipo?: 'text' | 'email' | 'password' | 'number'
error?: string
}>()
// Emits tipados
const emit = defineEmits<{
blur: [evento: FocusEvent]
enter: [valor: string]
}>()
function manejarBlur(e: FocusEvent): void {
emit('blur', e)
}
function manejarEnter(): void {
emit('enter', modelo.value)
}
</script>
<template>
<div class="campo">
<label>
{{ etiqueta }}
<input
:type="tipo"
:value="modelo"
:placeholder="placeholder"
:class="{ 'error': error }"
@input="modelo = ($event.target as HTMLInputElement).value"
@blur="manejarBlur"
@keyup.enter="manejarEnter"
/>
</label>
<span v-if="error" class="mensaje-error" role="alert">{{ error }}</span>
</div>
</template>
<style scoped>
.campo { display: flex; flex-direction: column; gap: 0.25rem; }
input.error { border-color: red; }
.mensaje-error { color: red; font-size: 0.875rem; }
</style>
<script setup lang="ts">
import { computed } from 'vue'
// Vue 3.5 — Reactive Props Destructure
// Los valores desestructurados SON reactivos (rastreados por el compilador)
const {
nombre,
apellido,
activo = true,
} = defineProps<{
nombre: string
apellido: string
activo?: boolean
}>()
// Esto SÍ funciona en Vue 3.5 — la desestructuración es reactiva
const nombreCompleto = computed(() => `${nombre} ${apellido}`)
const estado = computed(() => activo ? 'Activo' : 'Inactivo')
</script>
<template>
<div :class="{ activo }">
<p>{{ nombreCompleto }}</p>
<span>{{ estado }}</span>
</div>
</template>
Inicia sesión para guardar tu progreso