DEV Community

Cover image for ๐Ÿง  How defineModel Works in Vue 3 โ€” A Modern Deep Dive
Youssef Abdulaziz
Youssef Abdulaziz

Posted on

๐Ÿง  How defineModel Works in Vue 3 โ€” A Modern Deep Dive

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>
Enter fullscreen mode Exit fullscreen mode

TextInput.vue

<script setup>
   const modelValue = defineModel()
</script>

<template>
    <input v-model="modelValue" />
</template>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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]]
  )
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Breaking It Down

So what exactly is happening here?

  1. โšก Props & Emits are generated for you:
   props: { modelValue: {}, modelModifiers: {} },
   emits: ["update:modelValue"]
Enter fullscreen mode Exit fullscreen mode
  1. โšก defineModel turns into useModel:
   const modelValue = useModel(__props, "modelValue")
Enter fullscreen mode Exit fullscreen mode

This creates a local ref that:

  • Reads from props.modelValue
  • Emits update:modelValue when assigned to
  1. โšก 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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)