defineModel() en Vue 3.4+: La evolución elegante del v-model en componentes

A partir de Vue 3.4 y disponible en la actual versión 3.5+, defineModel() se ha convertido en una de las macros más potentes dentro del <script setup>. Esta nueva sintaxis te permite trabajar con v-model de forma más clara, concisa y tipada, eliminando la necesidad de declarar manualmente props y emits para la comunicación padre-hijo. Si estás trabajando con Vue 3.5+, ya deberías estar usándolo. ¿La clave? Menos boilerplate, más fluidez, y una integración impecable con los modifiers y múltiples v-model. ¡Dile adiós a modelValue verboso y hola a bindings con estilo!

¿Qué es defineModel()?

Es una macro especial disponible únicamente en el contexto de <script setup>. Nos permite declarar una prop reactiva que está automáticamente conectada con el v-model del componente padre.

En resumen: menos código, menos errores, más claridad.

Ejemplo básico: Un input personalizado

<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel({ type: String })
</script>

<template>
  <input v-model="model" placeholder="Escribe algo..." />
</template>
Y lo usas así:
<!-- ParentComponent.vue -->
<script setup lang="ts">
const nombre = ref('')
</script>

<template>
  <CustomInput v-model="nombre" />
</template>

Automáticamente:

  • Se declara la prop modelValue

  • Se emite update:modelValue al cambiar

  • ¡No necesitas escribir props ni emits manualmente!

Múltiples v-models: ¿Y si quiero algo más que modelValue?

Fácil. Solo le das un nombre:
<!-- CounterComponent.vue -->
<script setup lang="ts">
const count = defineModel('count', { type: Number, default: 0 })

function incrementar() {
  count.value++
}
</script>

<template>
  <button @click="incrementar">Contador: {{ count }}</button>
</template>
En el padre:
<!-- ParentComponent.vue -->
<script setup lang="ts">
const clicks = ref(5)
</script>

<template>
  <Counter v-model:count="clicks" />
</template>

Cuidado con los valores por defecto

Un error común: declarar un valor por defecto en el hijo, pero no pasar nada desde el padre. Esto puede causar desincronización.

Consejo: si vas a usar default, asegúrate de manejar correctamente el valor inicial desde el padre, o usar una sincronización manual si es necesario.

¿Y los modificadores? .trim, .number, .lazy...

Sí, también funcionan. Se puede capturar y personalizar su comportamiento:

<script setup lang="ts">
const [texto, modifiers] = defineModel()

const [modelValue, modelModifiers] = defineModel({
  set(value) {
    if (modelModifiers.trim) {
      return value.trim()
    }
    return value
  }
})
</script>

Ahora en el padre:

<MyInput v-model.trim="mensaje" />

El set() se encarga de aplicar .trim antes de enviar el valor modificado al padre.

Conclusión

defineModel() es un game changer para quienes usamos <script setup> en Vue 3.4+. Hace que trabajar con v-model en componentes sea más natural, más limpio y menos propenso a errores. Es ideal tanto para componentes simples como para interfaces complejas con múltiples bindings.