Vue 3.4 introduced a shiny new compiler macro called defineModel
, and itโs one of those features that makes you wonder: why did we ever write props
+ emits
manually?
If youโve ever built components that need v-model
, you know the drill: props for modelValue
, an emit for update:modelValue
, and a bit of boilerplate to wire them together. defineModel
takes all of that and turns it into a single, clean line of code.
Letโs peel back the curtain and see whatโs really happening.
โจ The Simple Example
Hereโs a minimal parent + child setup:
App.vue
<script setup>
import { ref } from 'vue'
import TextInput from './TextInput.vue'
const textModel = ref('')
</script>
<template>
<TextInput v-model="textModel" />
<p>Name: {{ textModel }}</p>
</template>
TextInput.vue
<script setup>
const modelValue = defineModel()
</script>
<template>
<input v-model="modelValue" />
</template>
Thatโs it. No props. No emits. Just defineModel
.
๐ ๏ธ What Vue Compiles It Into
When you hit save, Vueโs compiler does the heavy lifting. defineModel()
gets expanded into a useModel
call plus generated props + emits.
TextInput.vue (source) | Compiled (JS)
<script setup>
const modelValue = defineModel()
</script>
<template>
<input v-model="modelValue" />
</template>
import { useModel as _useModel } from 'vue'
const __sfc__ = {
__name: 'TextInput',
props: {
"modelValue": {},
"modelModifiers": {},
},
emits: ["update:modelValue"],
setup(__props, { expose: __expose }) {
__expose();
// ๐ defineModel turns into useModel
const modelValue = _useModel(__props, "modelValue")
return { modelValue }
}
}
function render(_ctx, _cache, $props, $setup) {
return _withDirectives(
_openBlock(),
_createElementBlock("input", {
"onUpdate:modelValue": $event => ($setup.modelValue = $event)
}, null, 512),
[[_vModelText, $setup.modelValue]]
)
}
๐ Breaking It Down
So what exactly is happening here?
- โก Props & Emits are generated for you:
props: { modelValue: {}, modelModifiers: {} },
emits: ["update:modelValue"]
-
โก
defineModel
turns intouseModel
:
const modelValue = useModel(__props, "modelValue")
This creates a local ref that:
- Reads from
props.modelValue
- Emits
update:modelValue
when assigned to
-
โก Template reactivity stays clean:
The
v-model="modelValue"
in your template is just sugar. Under the hood, it compiles into:
onUpdate:modelValue: $event => ($setup.modelValue = $event)
Which calls the setter from useModel
and triggers the parent update.
๐ Why This Matters
-
Less boilerplate โ No more writing
props
+emits
manually. -
Cleaner mental model โ Treat your model like a local
ref
. -
Type safety โ Works with TypeScript generics (
defineModel<string>()
). -
Multiple models โ Add more with
defineModel('name')
,defineModel('content')
.
๐ง The Mental Model
Think of defineModel
like this:
Parent v-model โ Childโs local ref (via useModel)
- The child sees a normal reactive ref (
modelValue
). - The parent gets updates automatically, without extra emits.
- Both stay in sync.
๐ก Final Thoughts
Vueโs defineModel
is one of those quality-of-life upgrades that make your codebase cleaner and your brain lighter. Instead of wiring up props and emits by hand, you just declare a reactive ref, and Vue does the rest.
Itโs reactivity the way it should feel: local when you need it, two-way when you want it.
Top comments (0)