[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-vue-weekly-definemodel-v-model-contract":44,"search-suggestions":60,"quiz-article-vue-weekly-definemodel-v-model-contract":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0af18-2e09-770d-907e-a11709e4d355","vue-weekly-definemodel-v-model-contract","PRACTICE_QUIZ","Vue defineModel & the v-model Contract","Nine questions on what v-model compiles to on a component, how defineModel's returned ref behaves with and without a parent binding, and how named models and modifiers work.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},9,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",124,{"slug":66,"name":67,"articles":68},"javascript","Javascript",103,{"slug":70,"name":71,"articles":72},"frontend","Frontend",79,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",48,{"slug":78,"name":79,"articles":80},"css","Css",40,{"slug":82,"name":83,"articles":84},"typescript","Typescript",18,{"slug":86,"name":87,"articles":88},"performance","Performance",16,{"slug":90,"name":91,"articles":92},"react","React",15,{"slug":94,"name":95,"articles":96},"browser","Browser",11,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":51},"html","Html",{"slug":104,"name":105,"articles":106},"accessibility","Accessibility",8,{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":124,"_count":129,"playground":131,"body":133,"bodyMd":477,"seo":478,"translationGroupId":480,"series":481,"podcastUrl":52,"verticalId":5,"thread":504,"assessments":506,"translations":509,"quiz":511},"01a0af18-2d15-7347-b2f4-6cdfa2ce56fa","Vue defineModel: The v-model Contract, Explained","Vue's defineModel macro replaces the modelValue prop and update:modelValue event with one ref. The exact v-model contract behind it, verified against Vue 3.5.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-definemodel-v-model-contract.png",13,"2026-10-05T06:11:02.623Z",67,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":122,"name":123},"vue","Vue",[125,126,127,128],{"slug":122,"name":123,"color":52},{"slug":66,"name":67,"color":52},{"slug":74,"name":75,"color":52},{"slug":62,"name":63,"color":52},{"assessments":130},1,{"slug":46,"title":132},"Vue defineModel &amp; the v-model contract — interactive playground",{"blocks":134,"version":130},[135,139,144,147,156,159,162,165,176,179,182,185,190,193,196,200,203,206,209,212,218,221,224,227,231,235,238,241,244,249,252,255,258,262,266,269,272,275,279,283,286,289,292,295,298,301,304,307,310,313,316,319,322,330,333,337,340,344,347,351,354,358,361,365,368,371,374,377,380,383,386,426,430,433,441,444,447,450,453,459,462,465,468,471],{"id":136,"html":137,"type":138},"b1","\u003Cp>You&#39;ve written this a dozen times: declare a \u003Ccode>modelValue\u003C\u002Fcode> prop, declare an \u003Ccode>update:modelValue\u003C\u002Fcode> emit, wire a computed&#39;s \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode> between them, and hope you spelled the event name identically in both places. Misspell it — \u003Ccode>updata:modelValue\u003C\u002Fcode>, \u003Ccode>update:modelvalue\u003C\u002Fcode>, anything — and nothing errors; at most a dev-mode warning scrolls past. The parent&#39;s \u003Ccode>v-model\u003C\u002Fcode> just stops updating, and you&#39;re left bisecting a component that looks correct. Vue 3.4 replaced that whole dance with a single macro, \u003Ccode>defineModel\u003C\u002Fcode>. But it isn&#39;t new magic bolted onto \u003Ccode>v-model\u003C\u002Fcode> — it&#39;s Vue writing the same prop-and-event contract for you, and once you can see that contract, every edge case here becomes predictable instead of surprising.\u003C\u002Fp>","paragraph",{"id":140,"html":141,"text":142,"type":143,"level":31},"b2","What You&#39;ll Learn","What You'll Learn","heading",{"id":145,"html":146,"type":138},"b3","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":148,"type":149,"items":150,"ordered":18},"b4","list",[151,152,153,154,155],"Explain exactly what \u003Ccode>v-model\u003C\u002Fcode> on a component compiles down to","Replace the manual prop\u002Femit\u002Fcomputed pattern with \u003Ccode>defineModel\u003C\u002Fcode> correctly","Give a model a default value, make it required, and type it in TypeScript","Bind multiple independent named models on one component","Read the modifiers a parent attaches to \u003Ccode>v-model\u003C\u002Fcode> and transform values with \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode>",{"id":157,"html":158,"text":158,"type":143,"level":31},"b5","Who This Is For",{"id":160,"html":161,"type":138},"b6","\u003Cp>You&#39;ve built Vue 3 components with \u003Ccode>&lt;script setup&gt;\u003C\u002Fcode>, used \u003Ccode>defineProps\u003C\u002Fcode> and \u003Ccode>defineEmits\u003C\u002Fcode>, and put \u003Ccode>v-model\u003C\u002Fcode> on a native \u003Ccode>&lt;input&gt;\u003C\u002Fcode> at least once. This article is about what happens when you put \u003Ccode>v-model\u003C\u002Fcode> on \u003Cem>your own\u003C\u002Fem> component. If the read\u002Fwrite tracking underneath \u003Ccode>ref\u003C\u002Fcode> and \u003Ccode>reactive\u003C\u002Fcode> is still fuzzy, \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fvue-reactivity-explained-ref-vs-reactive-cheat-sheet-4nij\">Vue Reactivity: ref vs reactive\u003C\u002Fa> builds that foundation — useful background, not required reading.\u003C\u002Fp>",{"id":163,"html":164,"text":164,"type":143,"level":31},"b7","Table of Contents",{"id":166,"type":149,"items":167,"ordered":18},"b8",[168,169,170,171,172,173,174,175],"\u003Ca href=\"#the-problem-the-propemit-dance-by-hand\">The Problem: The Prop\u002FEmit Dance, By Hand\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-v-model-is-always-a-prop-and-an-event\">The Mental Model: v-model Is Always a Prop and an Event\u003C\u002Fa>","\u003Ca href=\"#building-it-up-how-definemodel-actually-works\">Building It Up: How defineModel Actually Works\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge Cases and Gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best Practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat Sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key Takeaways\u003C\u002Fa>",{"id":177,"html":178,"type":138},"b9","\u003Cp>This is written against \u003Cstrong>Vue 3.5.x\u003C\u002Fstrong> (verified 3.5.43, released September 2026). \u003Ccode>defineModel\u003C\u002Fcode> has been stable, no-flag-required syntax since Vue 3.4 — nothing here needs the experimental flag that 3.3 required, and none of it changes in the 3.6 release candidate currently in testing: the only \u003Ccode>defineModel\u003C\u002Fcode>\u002F\u003Ccode>useModel\u003C\u002Fcode>-related fix in 3.6 so far is a Vapor-mode-only regression fix, and this article doesn&#39;t use Vapor mode.\u003C\u002Fp>",{"id":180,"html":181,"text":181,"type":143,"level":31},"b10","The Problem: The Prop\u002FEmit Dance, By Hand",{"id":183,"html":184,"type":138},"b11","\u003Cp>Say you&#39;re building a \u003Ccode>CurrencyInput\u003C\u002Fcode> component that a parent binds with \u003Ccode>v-model\u003C\u002Fcode>. Before \u003Ccode>defineModel\u003C\u002Fcode>, the idiomatic way to do this was a \u003Ccode>computed\u003C\u002Fcode> with a \u003Ccode>get\u003C\u002Fcode> and a \u003Ccode>set\u003C\u002Fcode>:\u003C\u002Fp>",{"id":186,"code":187,"type":188,"language":122,"highlight":189},"b12","\u003C!-- CurrencyInput.vue — the way you had to write it before Vue 3.4 -->\n\u003Cscript setup>\nimport { computed } from 'vue'\n\nconst props = defineProps({\n  modelValue: { type: Number, default: 0 },\n})\nconst emit = defineEmits(['update:modelValue'])\n\n\u002F\u002F A prop can't be written to directly — Vue warns and the write is dropped.\n\u002F\u002F So you build a local, writable stand-in with a get\u002Fset computed.\nconst model = computed({\n  get: () => props.modelValue,\n  set: (value) => emit('update:modelValue', value),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput\n    type=\"number\"\n    :value=\"model\"\n    @input=\"model = Number($event.target.value)\"\n  \u002F>\n\u003C\u002Ftemplate>","code",[],{"id":191,"html":192,"type":138},"b13","\u003Cp>This works, and it isn&#39;t wrong — but look at what it costs. The prop name (\u003Ccode>modelValue\u003C\u002Fcode>) and the event name (\u003Ccode>update:modelValue\u003C\u002Fcode>) have to match a convention exactly, in two separate declarations, with no compiler check tying them together. Get the emit name wrong in both places and \u003Ccode>v-model\u003C\u002Fcode> on the parent&#39;s side just does nothing: no warning, no error, no update (wrong in only one place, dev mode at least warns). Multiply this by every component with a two-way binding, and by every component that needs more than one — a \u003Ccode>firstName\u003C\u002Fcode>\u002F\u003Ccode>lastName\u003C\u002Fcode> pair, say — and you&#39;re hand-writing the same four-line skeleton over and over, each copy a fresh chance to typo an event name.\u003C\u002Fp>",{"id":194,"html":195,"type":138},"b14","\u003Cp>\u003Ccode>defineModel\u003C\u002Fcode> collapses that entire block into one line:\u003C\u002Fp>",{"id":197,"code":198,"type":188,"language":122,"highlight":199},"b15","\u003Cscript setup>\nconst model = defineModel({ default: 0 })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput type=\"number\" v-model.number=\"model\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":201,"html":202,"type":138},"b16","\u003Cp>That&#39;s not a different feature — it&#39;s the same prop, the same event, and the same get\u002Fset relationship, generated for you from a single declaration.\u003C\u002Fp>",{"id":204,"html":205,"text":205,"type":143,"level":31},"b17","The Mental Model: v-model Is Always a Prop and an Event",{"id":207,"html":208,"type":138},"b18","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> whatever syntax you write on the outside, \u003Ccode>v-model\u003C\u002Fcode> on a component always compiles to exactly two things — a prop bound to the current value, and a listener for an event that writes a new value back up. \u003Ccode>v-model=&quot;x&quot;\u003C\u002Fcode> on \u003Ccode>&lt;MyInput&gt;\u003C\u002Fcode> is shorthand for \u003Ccode>:model-value=&quot;x&quot; @update:model-value=&quot;x = $event&quot;\u003C\u002Fcode>. That&#39;s the entire contract. Every version of &quot;two-way binding on a component&quot; Vue has ever shipped — the \u003Ccode>value\u003C\u002Fcode>\u002F\u003Ccode>input\u003C\u002Fcode> convention in Vue 2, the \u003Ccode>modelValue\u003C\u002Fcode>\u002F\u003Ccode>update:modelValue\u003C\u002Fcode> convention in early Vue 3, and \u003Ccode>defineModel\u003C\u002Fcode> today — is just a different amount of \u003Cem>automation\u003C\u002Fem> over that same pair.\u003C\u002Fp>",{"id":210,"html":211,"type":138},"b19","\u003Cp>\u003Ccode>defineModel()\u003C\u002Fcode> is a compiler macro (available only inside \u003Ccode>&lt;script setup&gt;\u003C\u002Fcode>; outside it, the helper it compiles to — \u003Ccode>useModel(props, &#39;modelValue&#39;)\u003C\u002Fcode> — does the same job once you declare the prop and emit yourself) that, for one declaration, generates:\u003C\u002Fp>",{"id":213,"type":149,"items":214,"ordered":17},"b20",[215,216,217],"A prop (\u003Ccode>modelValue\u003C\u002Fcode> by default, or a name you choose) added to the component&#39;s props, exactly as if you&#39;d written it in \u003Ccode>defineProps\u003C\u002Fcode>.","An emit (\u003Ccode>update:modelValue\u003C\u002Fcode>) added to the component&#39;s emits, exactly as if you&#39;d written it in \u003Ccode>defineEmits\u003C\u002Fcode>.","A \u003Ccode>computed\u003C\u002Fcode>-like ref that reads the prop and, when written to, fires the emit — the same get\u002Fset relationship you&#39;d otherwise write by hand.",{"id":219,"html":220,"type":138},"b21","\u003Cp>The one behavior that isn&#39;t just &quot;the same thing, shorter&quot;: if the parent doesn&#39;t bind that prop at all, the ref defineModel returns doesn&#39;t stay \u003Ccode>undefined\u003C\u002Fcode> and read-only the way a plain prop would. It falls back to behaving like an ordinary local \u003Ccode>ref\u003C\u002Fcode>, seeded with whatever \u003Ccode>default\u003C\u002Fcode> you gave it, fully writable, with no parent to sync to. That fallback is what makes \u003Ccode>defineModel\u003C\u002Fcode> usable for genuinely optional two-way bindings — a component with a sensible built-in state that a parent can \u003Cem>optionally\u003C\u002Fem> take over.\u003C\u002Fp>",{"id":222,"html":223,"text":223,"type":143,"level":31},"b22","Building It Up: How defineModel Actually Works",{"id":225,"html":226,"text":226,"type":143,"level":43},"b23","The basic case",{"id":228,"code":229,"type":188,"language":122,"highlight":230},"b24","\u003C!-- CurrencyInput.vue -->\n\u003Cscript setup>\nconst model = defineModel({ default: 0 })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput type=\"number\" v-model.number=\"model\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":232,"code":233,"type":188,"language":122,"highlight":234},"b25","\u003C!-- Parent.vue -->\n\u003Cscript setup>\nimport { ref } from 'vue'\nimport CurrencyInput from '.\u002FCurrencyInput.vue'\n\nconst price = ref(19.99)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CCurrencyInput v-model=\"price\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":236,"html":237,"type":138},"b26","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>model\u003C\u002Fcode> inside \u003Ccode>CurrencyInput\u003C\u002Fcode> and \u003Ccode>price\u003C\u002Fcode> inside the parent are two different refs kept in sync by the prop\u002Femit pair defineModel wrote for you. Mutating \u003Ccode>model.value\u003C\u002Fcode> inside the child doesn&#39;t reach across the component boundary directly — it emits, and the parent&#39;s \u003Ccode>v-model\u003C\u002Fcode> catches that emit and writes \u003Ccode>price.value\u003C\u002Fcode> for you.\u003C\u002Fp>",{"id":239,"html":240,"text":240,"type":143,"level":43},"b27","Options: default, required, and type",{"id":242,"html":243,"type":138},"b28","\u003Cp>\u003Ccode>defineModel\u003C\u002Fcode> takes the same options a prop does, because under the hood it \u003Cem>is\u003C\u002Fem> one:\u003C\u002Fp>",{"id":245,"code":246,"type":188,"language":247,"highlight":248},"b29","\u002F\u002F A required model — TypeScript removes `undefined` from its type for you.\nconst model = defineModel\u003Cstring>({ required: true })\n\n\u002F\u002F An optional model with a default, typed explicitly.\nconst model = defineModel\u003Cstring | undefined>({ default: 'Untitled' })","ts",[],{"id":250,"html":251,"type":138},"b30","\u003Cp>Typing it explicitly matters: an untyped \u003Ccode>defineModel()\u003C\u002Fcode> in a \u003Ccode>.vue\u003C\u002Fcode> file with \u003Ccode>&lt;script setup lang=&quot;ts&quot;&gt;\u003C\u002Fcode> infers as loosely as an untyped prop would, so name the type rather than relying on inference from a call site that doesn&#39;t exist yet.\u003C\u002Fp>",{"id":253,"html":254,"text":254,"type":143,"level":43},"b31","Multiple, independent named models",{"id":256,"html":257,"type":138},"b32","\u003Cp>A component can expose more than one two-way binding by naming each \u003Ccode>defineModel\u003C\u002Fcode> call — this is the same \u003Ccode>v-model:propName\u003C\u002Fcode> syntax Vue has supported since named component v-models existed, now paired with the macro instead of hand-written props and emits:\u003C\u002Fp>",{"id":259,"code":260,"type":188,"language":122,"highlight":261},"b33","\u003C!-- NameFields.vue -->\n\u003Cscript setup>\nconst firstName = defineModel('firstName', { default: '' })\nconst lastName = defineModel('lastName', { default: '' })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"firstName\" placeholder=\"First name\" \u002F>\n  \u003Cinput v-model=\"lastName\" placeholder=\"Last name\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":263,"code":264,"type":188,"language":122,"highlight":265},"b34","\u003Ctemplate>\n  \u003CNameFields v-model:first-name=\"first\" v-model:last-name=\"last\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":267,"html":268,"type":138},"b35","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> each named \u003Ccode>defineModel\u003C\u002Fcode> call is its own independent prop\u002Femit pair. There&#39;s no shared state between \u003Ccode>firstName\u003C\u002Fcode> and \u003Ccode>lastName\u003C\u002Fcode> beyond what your component logic adds — they&#39;re two separate contracts, not one contract split in two.\u003C\u002Fp>",{"id":270,"html":271,"text":271,"type":143,"level":43},"b36","Modifiers, and transforming the value",{"id":273,"html":274,"type":138},"b37","\u003Cp>A parent can attach modifiers to any \u003Ccode>v-model\u003C\u002Fcode>, same as \u003Ccode>v-model.trim\u003C\u002Fcode> on a native input. Read them by destructuring the second value \u003Ccode>defineModel\u003C\u002Fcode> returns, and use \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode> to act on them:\u003C\u002Fp>",{"id":276,"code":277,"type":188,"language":122,"highlight":278},"b38","\u003C!-- TitleField.vue -->\n\u003Cscript setup>\nconst [model, modifiers] = defineModel({\n  set(value) {\n    return modifiers.capitalize && typeof value === 'string'\n      ? value.charAt(0).toUpperCase() + value.slice(1)\n      : value\n  },\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"model\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":280,"code":281,"type":188,"language":122,"highlight":282},"b39","\u003Ctemplate>\n  \u003CTitleField v-model.capitalize=\"title\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":284,"html":285,"type":138},"b40","\u003C!-- playground:start -->",{"id":287,"html":288,"text":288,"type":143,"level":31},"b41","🎮 Try it yourself",{"id":290,"html":291,"type":138},"b42","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-definemodel-v-model-contract\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":293,"html":294,"type":138},"b43","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":296,"html":297,"type":138},"b44","\u003C!-- playground:end -->",{"id":299,"html":300,"text":300,"type":143,"level":31},"b45","Edge Cases and Gotchas",{"id":302,"html":303,"type":138},"b46","\u003Cp>\u003Cstrong>Object and array defaults need a factory, not a literal.\u003C\u002Fstrong> This is the same rule as a regular prop default, and it&#39;s easy to forget because \u003Ccode>defineModel({ default: 0 })\u003C\u002Fcode> looks so much like a plain value assignment: \u003Ccode>default: []\u003C\u002Fcode> hands every instance without a bound value the \u003Cem>same\u003C\u002Fem> array, so mutating it in one unbound instance leaks into every other. Use \u003Ccode>default: () =&gt; []\u003C\u002Fcode> instead.\u003C\u002Fp>",{"id":305,"html":306,"type":138},"b47","\u003Cp>\u003Cstrong>No parent binding means a local ref, not \u003Ccode>undefined\u003C\u002Fcode>.\u003C\u002Fstrong> This is the behavior that makes \u003Ccode>defineModel\u003C\u002Fcode> genuinely different from a plain required-less prop, and it&#39;s also the one detail worth testing explicitly: a component you render without \u003Ccode>v-model\u003C\u002Fcode> at all should still work, driven entirely by its own default. Vue 3.4.1 extended the fallback to parents that pass the prop one-way, without an \u003Ccode>update:\u003C\u002Fcode> listener, so on 3.5 it&#39;s simply the documented behavior, not a caveat to work around.\u003C\u002Fp>",{"id":308,"html":309,"type":138},"b48","\u003Cp>\u003Cstrong>A \u003Ccode>default\u003C\u002Fcode> plus a parent bound to \u003Ccode>undefined\u003C\u002Fcode> desyncs.\u003C\u002Fstrong> With \u003Ccode>&lt;Child v-model=&quot;myRef&quot; \u002F&gt;\u003C\u002Fcode> and \u003Ccode>myRef = ref()\u003C\u002Fcode>, the child shows its \u003Ccode>default\u003C\u002Fcode> while the parent&#39;s \u003Ccode>myRef\u003C\u002Fcode> stays \u003Ccode>undefined\u003C\u002Fcode> until the child writes. The Vue docs call this out as a warning: if the parent binds the model, give its ref a real starting value rather than leaning on the child&#39;s \u003Ccode>default\u003C\u002Fcode>.\u003C\u002Fp>",{"id":311,"html":312,"type":138},"b49","\u003Cp>\u003Cstrong>Don&#39;t declare the same prop name in both \u003Ccode>defineProps\u003C\u002Fcode> and \u003Ccode>defineModel\u003C\u002Fcode>.\u003C\u002Fstrong> \u003Ccode>defineModel(&#39;title&#39;)\u003C\u002Fcode> already declares a \u003Ccode>title\u003C\u002Fcode> prop. List \u003Ccode>title\u003C\u002Fcode> in \u003Ccode>defineProps\u003C\u002Fcode> too and the compiler won&#39;t complain — it merges both, and defineModel&#39;s options silently replace yours for that key. Any prop that isn&#39;t part of a \u003Ccode>v-model\u003C\u002Fcode> binding still belongs in an ordinary \u003Ccode>defineProps\u003C\u002Fcode>.\u003C\u002Fp>",{"id":314,"html":315,"type":138},"b50","\u003Cp>\u003Cstrong>\u003Ccode>get\u003C\u002Fcode> runs on every read; \u003Ccode>set\u003C\u002Fcode> runs only when the component itself assigns \u003Ccode>model.value\u003C\u002Fcode>.\u003C\u002Fstrong> Values arriving from the parent never pass through \u003Ccode>set\u003C\u002Fcode>, and with no parent binding \u003Ccode>set\u003C\u002Fcode>&#39;s return value is only emitted — the local ref keeps the raw value. Keep both transforms pure and cheap — no side effects, no async work. They&#39;re formatting hooks, not lifecycle hooks.\u003C\u002Fp>",{"id":317,"html":318,"type":138},"b51","\u003Cp>\u003Cstrong>Modifiers on an unnamed model and a named model are separate namespaces.\u003C\u002Fstrong> \u003Ccode>v-model.trim\u003C\u002Fcode> sets modifiers for the default (\u003Ccode>modelValue\u003C\u002Fcode>) model; \u003Ccode>v-model:title.trim\u003C\u002Fcode> sets modifiers scoped to the \u003Ccode>title\u003C\u002Fcode> model only. Destructure the modifiers from the matching \u003Ccode>defineModel\u003C\u002Fcode> call, not a shared object.\u003C\u002Fp>",{"id":320,"html":321,"text":321,"type":143,"level":31},"b52","Best Practices",{"id":323,"type":149,"items":324,"ordered":18},"b53",[325,326,327,328,329],"\u003Cstrong>Reach for \u003Ccode>defineModel\u003C\u002Fcode> for anything inherently two-way\u003C\u002Fstrong> — form controls, toggles, a value the child both displays and edits. It&#39;s the direct replacement for the old computed get\u002Fset pattern, not a new category of API.","\u003Cstrong>Prefer named models over overloading one \u003Ccode>modelValue\u003C\u002Fcode>\u003C\u002Fstrong> the moment a component genuinely exposes more than one independent piece of editable state. Two named models are clearer than one object-shaped model the child has to destructure.","\u003Cstrong>Type every \u003Ccode>defineModel\u003C\u002Fcode> explicitly in TypeScript.\u003C\u002Fstrong> An inferred type from a macro with no call-site arguments is easy to get wrong silently; write \u003Ccode>defineModel&lt;string&gt;()\u003C\u002Fcode> rather than leaning on inference.","\u003Cstrong>Don&#39;t use \u003Ccode>v-model\u003C\u002Fcode> to fake a read-only prop.\u003C\u002Fstrong> If the child never writes back, it&#39;s a normal prop, not a model — \u003Ccode>v-model\u003C\u002Fcode> signals &quot;this can flow both ways,&quot; and using it where that&#39;s untrue misleads the next person who reads the parent&#39;s template.","\u003Cstrong>Don&#39;t wire \u003Ccode>defineModel\u003C\u002Fcode> straight into a Pinia store.\u003C\u002Fstrong> \u003Ccode>v-model\u003C\u002Fcode> is a parent\u002Fchild contract; a store is global state. Write to store actions directly and keep the two mechanisms separate, or you&#39;ll end up debugging which one actually owns the value.",{"id":331,"html":332,"text":332,"type":143,"level":31},"b54","FAQ",{"id":334,"html":335,"text":336,"type":143,"level":43},"b55","Does \u003Ccode>defineModel\u003C\u002Fcode> replace \u003Ccode>defineProps\u003C\u002Fcode> entirely?","Does defineModel replace defineProps entirely?",{"id":338,"html":339,"type":138},"b56","\u003Cp>No. It only replaces the declaration for props that participate in a \u003Ccode>v-model\u003C\u002Fcode> binding. Every other prop on the component still goes in an ordinary \u003Ccode>defineProps\u003C\u002Fcode>.\u003C\u002Fp>",{"id":341,"html":342,"text":343,"type":143,"level":43},"b57","Can I use \u003Ccode>defineModel\u003C\u002Fcode> outside \u003Ccode>&lt;script setup&gt;\u003C\u002Fcode>?","Can I use defineModel outside \u003Cscript setup>?",{"id":345,"html":346,"type":138},"b58","\u003Cp>Not the macro itself — it only exists inside \u003Ccode>&lt;script setup&gt;\u003C\u002Fcode>. But the helper it compiles to, \u003Ccode>useModel()\u003C\u002Fcode> (3.4+), works in a plain \u003Ccode>setup()\u003C\u002Fcode>: declare the prop and \u003Ccode>update:\u003C\u002Fcode> emit yourself, then \u003Ccode>const model = useModel(props, &#39;modelValue&#39;)\u003C\u002Fcode>.\u003C\u002Fp>",{"id":348,"html":349,"text":350,"type":143,"level":43},"b59","Why does \u003Ccode>v-model\u003C\u002Fcode> on my component silently do nothing?","Why does v-model on my component silently do nothing?",{"id":352,"html":353,"type":138},"b60","\u003Cp>Before \u003Ccode>defineModel\u003C\u002Fcode>, this was almost always a mismatched prop\u002Femit name — \u003Ccode>modelValue\u003C\u002Fcode> declared in props but \u003Ccode>update:modalValue\u003C\u002Fcode> (typo) in the emit, or vice versa. \u003Ccode>defineModel\u003C\u002Fcode> removes that failure mode entirely, since one declaration generates both sides. If it&#39;s still not updating, check that the parent binds the name you declared (\u003Ccode>v-model:title\u003C\u002Fcode> for \u003Ccode>defineModel(&#39;title&#39;)\u003C\u002Fcode>), and that you haven&#39;t re-declared the prop in \u003Ccode>defineProps\u003C\u002Fcode> — that compiles silently and defineModel&#39;s options replace yours.\u003C\u002Fp>",{"id":355,"html":356,"text":357,"type":143,"level":43},"b61","Do I need to add the model&#39;s event to \u003Ccode>defineEmits\u003C\u002Fcode> as well?","Do I need to add the model's event to defineEmits as well?",{"id":359,"html":360,"type":138},"b62","\u003Cp>No — \u003Ccode>defineModel\u003C\u002Fcode> declares the prop and the emit for you. Adding the same event again in \u003Ccode>defineEmits\u003C\u002Fcode> is redundant but harmless — the compiler merges both lists.\u003C\u002Fp>",{"id":362,"html":363,"text":364,"type":143,"level":43},"b63","Is \u003Ccode>defineModel\u003C\u002Fcode> available in Vue 2?","Is defineModel available in Vue 2?",{"id":366,"html":367,"type":138},"b64","\u003Cp>No. Vue 2 has no compiler macro for this; components there declare \u003Ccode>value\u003C\u002Fcode>\u002F\u003Ccode>input\u003C\u002Fcode> (Vue 2&#39;s default model event, before the \u003Ccode>modelValue\u003C\u002Fcode>\u002F\u003Ccode>update:modelValue\u003C\u002Fcode> convention Vue 3 introduced) by hand, and that pattern doesn&#39;t change in the Vue 2 line. Treat any Vue 2 example that mentions \u003Ccode>defineModel\u003C\u002Fcode> as a mistake, not a version difference.\u003C\u002Fp>",{"id":369,"html":370,"type":138},"b65","\u003C!-- quiz:start -->",{"id":372,"html":373,"text":373,"type":143,"level":31},"b66","🧠 Test yourself",{"id":375,"html":376,"type":138},"b67","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-definemodel-v-model-contract\u002Fquiz\">Take the 9-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":378,"html":379,"type":138},"b68","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":381,"html":382,"type":138},"b69","\u003C!-- quiz:end -->",{"id":384,"html":385,"text":385,"type":143,"level":31},"b70","Cheat Sheet",{"id":387,"head":388,"rows":392,"type":425},"b71",[389,390,391],"Task","Code","Notes",[393,397,401,405,409,413,417,421],[394,395,396],"Basic model","\u003Ccode>const model = defineModel()\u003C\u002Fcode>","Prop \u003Ccode>modelValue\u003C\u002Fcode> + emit \u003Ccode>update:modelValue\u003C\u002Fcode>, generated",[398,399,400],"With default","\u003Ccode>defineModel({ default: 0 })\u003C\u002Fcode>","Object\u002Farray defaults need a factory: \u003Ccode>default: () =&gt; []\u003C\u002Fcode>",[402,403,404],"Required","\u003Ccode>defineModel&lt;string&gt;({ required: true })\u003C\u002Fcode>","Removes \u003Ccode>undefined\u003C\u002Fcode> from the TS type",[406,407,408],"Named model","\u003Ccode>defineModel(&#39;title&#39;)\u003C\u002Fcode>","Parent binds with \u003Ccode>v-model:title\u003C\u002Fcode>",[410,411,412],"Multiple models","\u003Ccode>defineModel(&#39;a&#39;)\u003C\u002Fcode> + \u003Ccode>defineModel(&#39;b&#39;)\u003C\u002Fcode>","Fully independent prop\u002Femit pairs",[414,415,416],"Read modifiers","\u003Ccode>const [m, mods] = defineModel()\u003C\u002Fcode>","\u003Ccode>mods.yourModifier\u003C\u002Fcode> is \u003Ccode>true\u003C\u002Fcode> when the parent adds \u003Ccode>.yourModifier\u003C\u002Fcode>",[418,419,420],"Transform value","\u003Ccode>defineModel({ get(v) {…}, set(v) { return … } })\u003C\u002Fcode>","\u003Ccode>set\u003C\u002Fcode> runs on the child&#39;s writes (its result is what&#39;s emitted); \u003Ccode>get\u003C\u002Fcode> runs on every read",[422,423,424],"No parent binding","(default behavior)","Ref falls back to a local, fully writable ref seeded by \u003Ccode>default\u003C\u002Fcode>","table",{"id":427,"code":428,"type":188,"language":122,"highlight":429},"b72","\u003Cscript setup lang=\"ts\">\n\u002F\u002F The full pattern: a required, typed, transformed model with modifiers.\nconst [model, modifiers] = defineModel\u003Cstring>({\n  required: true,\n  set(value) {\n    return modifiers.trim ? value.trim() : value\n  },\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model.trim=\"model\" \u002F>\n\u003C\u002Ftemplate>",[],{"id":431,"html":432,"text":432,"type":143,"level":31},"b73","Key Takeaways",{"id":434,"type":149,"items":435,"ordered":18},"b74",[436,437,438,439,440],"\u003Ccode>v-model\u003C\u002Fcode> on a component is always a prop plus an event; \u003Ccode>defineModel\u003C\u002Fcode> doesn&#39;t change that contract, it generates it.","With no parent binding, \u003Ccode>defineModel\u003C\u002Fcode>&#39;s ref becomes a local, writable ref seeded by your \u003Ccode>default\u003C\u002Fcode> — not \u003Ccode>undefined\u003C\u002Fcode>.","Independent two-way bindings are named models (\u003Ccode>defineModel(&#39;name&#39;)\u003C\u002Fcode>), each its own prop\u002Femit pair.","Modifiers arrive as the second destructured value; transform the model with \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode>, not a watcher.","The same prop rules still apply underneath: factory functions for object\u002Farray defaults, explicit typing in TypeScript.",{"id":442,"html":443,"type":138},"b75","\u003Cp>The typo-prone emit dance from the top of this article is gone, but that was never the interesting part. \u003Ccode>v-model\u003C\u002Fcode> was never magic to begin with — it was always a prop and an event, wired by convention. \u003Ccode>defineModel\u003C\u002Fcode> just means you stop wiring it by hand, and everything you now know about that contract still applies exactly where the macro can&#39;t reach: get\u002Fset transforms, multiple named models, and the one moment it quietly falls back to a local ref.\u003C\u002Fp>",{"id":445,"html":446,"type":138},"b76","\u003Cp>What&#39;s the messiest custom \u003Ccode>v-model\u003C\u002Fcode> you&#39;ve had to debug — a typo&#39;d event name, a shared object default, or something else entirely? Drop it in the comments.\u003C\u002Fp>",{"id":448,"html":449,"type":138},"b77","\u003C!-- related:start -->",{"id":451,"html":452,"text":452,"type":143,"level":31},"b78","📚 Read next",{"id":454,"type":149,"items":455,"ordered":18},"b79",[456,457,458],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-composables-shared-state-trap\">Vue Composables: The Shared State Trap (+ Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-nexttick-batching-dom-updates\">Vue nextTick Explained: How DOM Updates Are Batched\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-reactivity-ref-vs-reactive\">Vue Reactivity Explained: ref vs reactive (+ Cheat Sheet)\u003C\u002Fa>",{"id":460,"html":461,"type":138},"b80","\u003C!-- related:end -->",{"id":463,"type":464},"b81","divider",{"id":466,"html":467,"type":138},"b82","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":469,"html":470,"type":138},"b83","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":472,"type":149,"items":473,"ordered":18},"b84",[474,475,476],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You've written this a dozen times: declare a `modelValue` prop, declare an `update:modelValue` emit, wire a computed's `get`\u002F`set` between them, and hope you spelled the event name identically in both places. Misspell it — `updata:modelValue`, `update:modelvalue`, anything — and nothing errors; at most a dev-mode warning scrolls past. The parent's `v-model` just stops updating, and you're left bisecting a component that looks correct. Vue 3.4 replaced that whole dance with a single macro, `defineModel`. But it isn't new magic bolted onto `v-model` — it's Vue writing the same prop-and-event contract for you, and once you can see that contract, every edge case here becomes predictable instead of surprising.\n\n## What You'll Learn\n\nBy the end of this article you'll be able to:\n\n- Explain exactly what `v-model` on a component compiles down to\n- Replace the manual prop\u002Femit\u002Fcomputed pattern with `defineModel` correctly\n- Give a model a default value, make it required, and type it in TypeScript\n- Bind multiple independent named models on one component\n- Read the modifiers a parent attaches to `v-model` and transform values with `get`\u002F`set`\n\n## Who This Is For\n\nYou've built Vue 3 components with `\u003Cscript setup>`, used `defineProps` and `defineEmits`, and put `v-model` on a native `\u003Cinput>` at least once. This article is about what happens when you put `v-model` on *your own* component. If the read\u002Fwrite tracking underneath `ref` and `reactive` is still fuzzy, [Vue Reactivity: ref vs reactive](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fvue-reactivity-explained-ref-vs-reactive-cheat-sheet-4nij) builds that foundation — useful background, not required reading.\n\n## Table of Contents\n\n- [The Problem: The Prop\u002FEmit Dance, By Hand](#the-problem-the-propemit-dance-by-hand)\n- [The Mental Model: v-model Is Always a Prop and an Event](#the-mental-model-v-model-is-always-a-prop-and-an-event)\n- [Building It Up: How defineModel Actually Works](#building-it-up-how-definemodel-actually-works)\n- [Edge Cases and Gotchas](#edge-cases-and-gotchas)\n- [Best Practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat Sheet](#cheat-sheet)\n- [Key Takeaways](#key-takeaways)\n\nThis is written against **Vue 3.5.x** (verified 3.5.43, released September 2026). `defineModel` has been stable, no-flag-required syntax since Vue 3.4 — nothing here needs the experimental flag that 3.3 required, and none of it changes in the 3.6 release candidate currently in testing: the only `defineModel`\u002F`useModel`-related fix in 3.6 so far is a Vapor-mode-only regression fix, and this article doesn't use Vapor mode.\n\n## The Problem: The Prop\u002FEmit Dance, By Hand\n\nSay you're building a `CurrencyInput` component that a parent binds with `v-model`. Before `defineModel`, the idiomatic way to do this was a `computed` with a `get` and a `set`:\n\n```vue\n\u003C!-- CurrencyInput.vue — the way you had to write it before Vue 3.4 -->\n\u003Cscript setup>\nimport { computed } from 'vue'\n\nconst props = defineProps({\n  modelValue: { type: Number, default: 0 },\n})\nconst emit = defineEmits(['update:modelValue'])\n\n\u002F\u002F A prop can't be written to directly — Vue warns and the write is dropped.\n\u002F\u002F So you build a local, writable stand-in with a get\u002Fset computed.\nconst model = computed({\n  get: () => props.modelValue,\n  set: (value) => emit('update:modelValue', value),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput\n    type=\"number\"\n    :value=\"model\"\n    @input=\"model = Number($event.target.value)\"\n  \u002F>\n\u003C\u002Ftemplate>\n```\n\nThis works, and it isn't wrong — but look at what it costs. The prop name (`modelValue`) and the event name (`update:modelValue`) have to match a convention exactly, in two separate declarations, with no compiler check tying them together. Get the emit name wrong in both places and `v-model` on the parent's side just does nothing: no warning, no error, no update (wrong in only one place, dev mode at least warns). Multiply this by every component with a two-way binding, and by every component that needs more than one — a `firstName`\u002F`lastName` pair, say — and you're hand-writing the same four-line skeleton over and over, each copy a fresh chance to typo an event name.\n\n`defineModel` collapses that entire block into one line:\n\n```vue\n\u003Cscript setup>\nconst model = defineModel({ default: 0 })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput type=\"number\" v-model.number=\"model\" \u002F>\n\u003C\u002Ftemplate>\n```\n\nThat's not a different feature — it's the same prop, the same event, and the same get\u002Fset relationship, generated for you from a single declaration.\n\n## The Mental Model: v-model Is Always a Prop and an Event\n\n**The mental model:** whatever syntax you write on the outside, `v-model` on a component always compiles to exactly two things — a prop bound to the current value, and a listener for an event that writes a new value back up. `v-model=\"x\"` on `\u003CMyInput>` is shorthand for `:model-value=\"x\" @update:model-value=\"x = $event\"`. That's the entire contract. Every version of \"two-way binding on a component\" Vue has ever shipped — the `value`\u002F`input` convention in Vue 2, the `modelValue`\u002F`update:modelValue` convention in early Vue 3, and `defineModel` today — is just a different amount of *automation* over that same pair.\n\n`defineModel()` is a compiler macro (available only inside `\u003Cscript setup>`; outside it, the helper it compiles to — `useModel(props, 'modelValue')` — does the same job once you declare the prop and emit yourself) that, for one declaration, generates:\n\n1. A prop (`modelValue` by default, or a name you choose) added to the component's props, exactly as if you'd written it in `defineProps`.\n2. An emit (`update:modelValue`) added to the component's emits, exactly as if you'd written it in `defineEmits`.\n3. A `computed`-like ref that reads the prop and, when written to, fires the emit — the same get\u002Fset relationship you'd otherwise write by hand.\n\nThe one behavior that isn't just \"the same thing, shorter\": if the parent doesn't bind that prop at all, the ref defineModel returns doesn't stay `undefined` and read-only the way a plain prop would. It falls back to behaving like an ordinary local `ref`, seeded with whatever `default` you gave it, fully writable, with no parent to sync to. That fallback is what makes `defineModel` usable for genuinely optional two-way bindings — a component with a sensible built-in state that a parent can *optionally* take over.\n\n## Building It Up: How defineModel Actually Works\n\n### The basic case\n\n```vue\n\u003C!-- CurrencyInput.vue -->\n\u003Cscript setup>\nconst model = defineModel({ default: 0 })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput type=\"number\" v-model.number=\"model\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n```vue\n\u003C!-- Parent.vue -->\n\u003Cscript setup>\nimport { ref } from 'vue'\nimport CurrencyInput from '.\u002FCurrencyInput.vue'\n\nconst price = ref(19.99)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CCurrencyInput v-model=\"price\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n**Key concept:** `model` inside `CurrencyInput` and `price` inside the parent are two different refs kept in sync by the prop\u002Femit pair defineModel wrote for you. Mutating `model.value` inside the child doesn't reach across the component boundary directly — it emits, and the parent's `v-model` catches that emit and writes `price.value` for you.\n\n### Options: default, required, and type\n\n`defineModel` takes the same options a prop does, because under the hood it *is* one:\n\n```ts\n\u002F\u002F A required model — TypeScript removes `undefined` from its type for you.\nconst model = defineModel\u003Cstring>({ required: true })\n\n\u002F\u002F An optional model with a default, typed explicitly.\nconst model = defineModel\u003Cstring | undefined>({ default: 'Untitled' })\n```\n\nTyping it explicitly matters: an untyped `defineModel()` in a `.vue` file with `\u003Cscript setup lang=\"ts\">` infers as loosely as an untyped prop would, so name the type rather than relying on inference from a call site that doesn't exist yet.\n\n### Multiple, independent named models\n\nA component can expose more than one two-way binding by naming each `defineModel` call — this is the same `v-model:propName` syntax Vue has supported since named component v-models existed, now paired with the macro instead of hand-written props and emits:\n\n```vue\n\u003C!-- NameFields.vue -->\n\u003Cscript setup>\nconst firstName = defineModel('firstName', { default: '' })\nconst lastName = defineModel('lastName', { default: '' })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"firstName\" placeholder=\"First name\" \u002F>\n  \u003Cinput v-model=\"lastName\" placeholder=\"Last name\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n```vue\n\u003Ctemplate>\n  \u003CNameFields v-model:first-name=\"first\" v-model:last-name=\"last\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n**Key concept:** each named `defineModel` call is its own independent prop\u002Femit pair. There's no shared state between `firstName` and `lastName` beyond what your component logic adds — they're two separate contracts, not one contract split in two.\n\n### Modifiers, and transforming the value\n\nA parent can attach modifiers to any `v-model`, same as `v-model.trim` on a native input. Read them by destructuring the second value `defineModel` returns, and use `get`\u002F`set` to act on them:\n\n```vue\n\u003C!-- TitleField.vue -->\n\u003Cscript setup>\nconst [model, modifiers] = defineModel({\n  set(value) {\n    return modifiers.capitalize && typeof value === 'string'\n      ? value.charAt(0).toUpperCase() + value.slice(1)\n      : value\n  },\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"model\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n```vue\n\u003Ctemplate>\n  \u003CTitleField v-model.capitalize=\"title\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-definemodel-v-model-contract\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Edge Cases and Gotchas\n\n**Object and array defaults need a factory, not a literal.** This is the same rule as a regular prop default, and it's easy to forget because `defineModel({ default: 0 })` looks so much like a plain value assignment: `default: []` hands every instance without a bound value the *same* array, so mutating it in one unbound instance leaks into every other. Use `default: () => []` instead.\n\n**No parent binding means a local ref, not `undefined`.** This is the behavior that makes `defineModel` genuinely different from a plain required-less prop, and it's also the one detail worth testing explicitly: a component you render without `v-model` at all should still work, driven entirely by its own default. Vue 3.4.1 extended the fallback to parents that pass the prop one-way, without an `update:` listener, so on 3.5 it's simply the documented behavior, not a caveat to work around.\n\n**A `default` plus a parent bound to `undefined` desyncs.** With `\u003CChild v-model=\"myRef\" \u002F>` and `myRef = ref()`, the child shows its `default` while the parent's `myRef` stays `undefined` until the child writes. The Vue docs call this out as a warning: if the parent binds the model, give its ref a real starting value rather than leaning on the child's `default`.\n\n**Don't declare the same prop name in both `defineProps` and `defineModel`.** `defineModel('title')` already declares a `title` prop. List `title` in `defineProps` too and the compiler won't complain — it merges both, and defineModel's options silently replace yours for that key. Any prop that isn't part of a `v-model` binding still belongs in an ordinary `defineProps`.\n\n**`get` runs on every read; `set` runs only when the component itself assigns `model.value`.** Values arriving from the parent never pass through `set`, and with no parent binding `set`'s return value is only emitted — the local ref keeps the raw value. Keep both transforms pure and cheap — no side effects, no async work. They're formatting hooks, not lifecycle hooks.\n\n**Modifiers on an unnamed model and a named model are separate namespaces.** `v-model.trim` sets modifiers for the default (`modelValue`) model; `v-model:title.trim` sets modifiers scoped to the `title` model only. Destructure the modifiers from the matching `defineModel` call, not a shared object.\n\n## Best Practices\n\n- **Reach for `defineModel` for anything inherently two-way** — form controls, toggles, a value the child both displays and edits. It's the direct replacement for the old computed get\u002Fset pattern, not a new category of API.\n- **Prefer named models over overloading one `modelValue`** the moment a component genuinely exposes more than one independent piece of editable state. Two named models are clearer than one object-shaped model the child has to destructure.\n- **Type every `defineModel` explicitly in TypeScript.** An inferred type from a macro with no call-site arguments is easy to get wrong silently; write `defineModel\u003Cstring>()` rather than leaning on inference.\n- **Don't use `v-model` to fake a read-only prop.** If the child never writes back, it's a normal prop, not a model — `v-model` signals \"this can flow both ways,\" and using it where that's untrue misleads the next person who reads the parent's template.\n- **Don't wire `defineModel` straight into a Pinia store.** `v-model` is a parent\u002Fchild contract; a store is global state. Write to store actions directly and keep the two mechanisms separate, or you'll end up debugging which one actually owns the value.\n\n## FAQ\n\n### Does `defineModel` replace `defineProps` entirely?\n\nNo. It only replaces the declaration for props that participate in a `v-model` binding. Every other prop on the component still goes in an ordinary `defineProps`.\n\n### Can I use `defineModel` outside `\u003Cscript setup>`?\n\nNot the macro itself — it only exists inside `\u003Cscript setup>`. But the helper it compiles to, `useModel()` (3.4+), works in a plain `setup()`: declare the prop and `update:` emit yourself, then `const model = useModel(props, 'modelValue')`.\n\n### Why does `v-model` on my component silently do nothing?\n\nBefore `defineModel`, this was almost always a mismatched prop\u002Femit name — `modelValue` declared in props but `update:modalValue` (typo) in the emit, or vice versa. `defineModel` removes that failure mode entirely, since one declaration generates both sides. If it's still not updating, check that the parent binds the name you declared (`v-model:title` for `defineModel('title')`), and that you haven't re-declared the prop in `defineProps` — that compiles silently and defineModel's options replace yours.\n\n### Do I need to add the model's event to `defineEmits` as well?\n\nNo — `defineModel` declares the prop and the emit for you. Adding the same event again in `defineEmits` is redundant but harmless — the compiler merges both lists.\n\n### Is `defineModel` available in Vue 2?\n\nNo. Vue 2 has no compiler macro for this; components there declare `value`\u002F`input` (Vue 2's default model event, before the `modelValue`\u002F`update:modelValue` convention Vue 3 introduced) by hand, and that pattern doesn't change in the Vue 2 line. Treat any Vue 2 example that mentions `defineModel` as a mistake, not a version difference.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 9-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-definemodel-v-model-contract\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## Cheat Sheet\n\n| Task | Code | Notes |\n| --- | --- | --- |\n| Basic model | `const model = defineModel()` | Prop `modelValue` + emit `update:modelValue`, generated |\n| With default | `defineModel({ default: 0 })` | Object\u002Farray defaults need a factory: `default: () => []` |\n| Required | `defineModel\u003Cstring>({ required: true })` | Removes `undefined` from the TS type |\n| Named model | `defineModel('title')` | Parent binds with `v-model:title` |\n| Multiple models | `defineModel('a')` + `defineModel('b')` | Fully independent prop\u002Femit pairs |\n| Read modifiers | `const [m, mods] = defineModel()` | `mods.yourModifier` is `true` when the parent adds `.yourModifier` |\n| Transform value | `defineModel({ get(v) {…}, set(v) { return … } })` | `set` runs on the child's writes (its result is what's emitted); `get` runs on every read |\n| No parent binding | (default behavior) | Ref falls back to a local, fully writable ref seeded by `default` |\n\n```vue\n\u003Cscript setup lang=\"ts\">\n\u002F\u002F The full pattern: a required, typed, transformed model with modifiers.\nconst [model, modifiers] = defineModel\u003Cstring>({\n  required: true,\n  set(value) {\n    return modifiers.trim ? value.trim() : value\n  },\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model.trim=\"model\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n## Key Takeaways\n\n- `v-model` on a component is always a prop plus an event; `defineModel` doesn't change that contract, it generates it.\n- With no parent binding, `defineModel`'s ref becomes a local, writable ref seeded by your `default` — not `undefined`.\n- Independent two-way bindings are named models (`defineModel('name')`), each its own prop\u002Femit pair.\n- Modifiers arrive as the second destructured value; transform the model with `get`\u002F`set`, not a watcher.\n- The same prop rules still apply underneath: factory functions for object\u002Farray defaults, explicit typing in TypeScript.\n\nThe typo-prone emit dance from the top of this article is gone, but that was never the interesting part. `v-model` was never magic to begin with — it was always a prop and an event, wired by convention. `defineModel` just means you stop wiring it by hand, and everything you now know about that contract still applies exactly where the macro can't reach: get\u002Fset transforms, multiple named models, and the one moment it quietly falls back to a local ref.\n\nWhat's the messiest custom `v-model` you've had to debug — a typo'd event name, a shared object default, or something else entirely? Drop it in the comments.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Vue Composables: The Shared State Trap (+ Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-composables-shared-state-trap)\n- [Vue nextTick Explained: How DOM Updates Are Batched](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-nexttick-batching-dom-updates)\n- [Vue Reactivity Explained: ref vs reactive (+ Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-reactivity-ref-vs-reactive)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":479,"description":110},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fvue-weekly-definemodel-v-model-contract","01a0af18-2d15-7347-b2f4-7005d0573ad4",{"name":482,"part":483,"total":483,"items":484},"Vue Deep Dive",5,[485,490,494,498,503],{"slug":486,"title":487,"publishedAt":488,"readingMinutes":489},"vue-weekly-reactivity-ref-vs-reactive","Vue Reactivity Explained: ref vs reactive (+ Cheat Sheet)","2026-08-24T13:57:38.080Z",14,{"slug":491,"title":492,"publishedAt":493,"readingMinutes":112},"vue-weekly-nexttick-batching-dom-updates","Vue nextTick Explained: How DOM Updates Are Batched","2026-09-14T12:19:00.606Z",{"slug":495,"title":496,"publishedAt":497,"readingMinutes":112},"vue-weekly-composables-shared-state-trap","Vue Composables: The Shared State Trap (+ Cheat Sheet)","2026-09-22T19:37:11.460Z",{"slug":499,"title":500,"publishedAt":501,"readingMinutes":502},"vue-weekly-computed-caching-invalidation","Vue computed(): What It Caches and When It Reruns","2026-09-30T12:10:53.362Z",12,{"slug":46,"title":109,"publishedAt":113,"readingMinutes":112},{"id":505,"locked":18},"01a0af18-2d88-7409-9ccb-d7453568482a",[507],{"id":45,"slug":46,"title":48,"_count":508},{"questions":51},[510],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":512,"questionCount":51},{"questions":51}]