In Vue 3, what does `v-model="query"` on a custom component expand to, and what must the child component provide for it to work?
answer
- a prop down, an event up
- default prop name changed in Vue 3
- update colon prefix
- one macro since 3.4
basics
~10 sOn a component, v-model="query" expands to a modelValue prop plus an onUpdate:modelValue listener that assigns the payload back to query. The child declares the prop and emits update:modelValue, or uses defineModel() since Vue 3.4.
solid answer
~30 sThe template compiler rewrites `<SearchBox v-model="query" />` into `:modelValue="query"` plus `@update:modelValue="$event => (query = $event)"`. So the child must accept a `modelValue` prop and emit `update:modelValue` with the new value, for example `defineProps(['modelValue'])` with `defineEmits(['update:modelValue'])`, binding `:value` and emitting on `input`. Since Vue 3.4 the recommended form is `const model = defineModel()`, which declares that prop and event and gives you a ref whose assignment emits the update. Vue 2 used a `value` prop and an `input` event instead; that contract is gone in Vue 3.
code
vue · 12 lines<!-- SearchBox.vue (Vue 3.4+) -->
<script setup lang="ts">
const model = defineModel<string>({ default: '' })
</script>
<template>
<input type="search" v-model="model" placeholder="Search" />
</template>
<!-- Parent: <SearchBox v-model="query" />
compiles to roughly
<SearchBox :modelValue="query" @update:modelValue="$event => (query = $event)" /> -->go deeper
Recall the pair: a modelValue prop going down and an update:modelValue event coming up, or defineModel() in Vue 3.4 and later.
Explain the compile-time rewrite, why mutating the prop fails, and how writing the two bindings by hand lets a parent intercept the value.
Use the expansion to debug broken two-way bindings, such as a migrated child still emitting input or a wrapper that forgets to re-emit.
Set a component-library convention for when inputs expose v-model versus explicit props and events, and keep it consistent across the library.
## v-model is sugar for a prop and an event On a **component** (as opposed to a native `<input>`), `v-model` does not create any hidden channel. The template compiler rewrites it into two ordinary bindings: - a **prop** named `modelValue`, bound to the parent's expression - a **listener** for the event `update:modelValue`, which assigns the emitted payload back to that expression So `<SearchBox v-model="query" />` compiles to roughly `<SearchBox :modelValue="query" @update:modelValue="$event => (query = $event)" />`. In the compiled render function the listener appears as the prop key `onUpdate:modelValue`. ## What the child must provide Because the parent now passes a prop and listens for an event, the child needs to honour both halves: 1. **Accept the value** as a prop named `modelValue` and render from it. 2. **Report changes** by emitting `update:modelValue` with the new value, never by mutating the prop. The explicit form, still valid in Vue 3.5 and the only form before 3.4: ```vue <script setup lang="ts"> const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script> <template> <input :value="props.modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" /> </template> ``` ## The 3.4+ shortcut: defineModel Since **Vue 3.4**, `<script setup>` offers the `defineModel()` macro, which the documentation now recommends. It declares the `modelValue` prop and the `update:modelValue` event for you and returns a **ref**: - reading `model.value` gives the current parent value - assigning `model.value = x` emits `update:modelValue` with `x` - the ref can be passed straight to an inner `<input v-model="model">` | Approach | Declares | You write | |---|---|---| | props + emits | `modelValue` prop, `update:modelValue` event | `:value` binding and an `emit` call | | `defineModel()` (3.4+) | the same prop and event, generated | `v-model="model"` on the inner element | ## Why the names matter in interviews The expansion explains several behaviours candidates are asked about: - A child that **mutates `props.modelValue`** gets a readonly warning and the parent never learns about the change, because nothing emitted the event. - A child that emits `input` instead of `update:modelValue` is using the **Vue 2 contract**; in Vue 3 the parent's `v-model` is not listening for that event. - Because it is just a prop and an event, a parent can write the two bindings by hand when it wants to intercept the value, for example to clamp or debounce it before assigning. ## Listening without v-model Because the expansion produces an ordinary event, a parent can react to changes without owning a two-way binding at all: - `@update:modelValue="onQueryChange"` runs a handler on every change, for example to log or debounce - `:modelValue="query"` alone passes a value down without accepting changes back - combining the two by hand gives the parent full control over what is stored This is also how you test the contract: mount the child with a `modelValue` prop and assert that it **emitted** `update:modelValue` with the expected payload, instead of asserting on the parent's state. ## A short checklist when component v-model does not update 1. Does the child declare `modelValue` (or use `defineModel`)? An undeclared prop falls through to the root element as an attribute. 2. Does the child emit `update:modelValue`, spelled exactly, rather than `input` or `change`? 3. Does the child emit the **new** value, not the event object? 4. Is the parent binding a writable expression, such as a ref or a reactive property, rather than a computed without a setter? Each item maps to one half of the expansion, which is why knowing it by heart makes these bugs quick to find. ## Arguments and modifiers build on the same rule `v-model:title` swaps the names to a `title` prop and an `update:title` event, which is what makes several `v-model`s on one component possible. Modifiers such as `v-model.capitalize` travel to the child as an extra prop, `modelModifiers` for the default model. Both are variations of the same compile-time rewrite, not separate mechanisms.
- What happens if a Vue 3 child mutates props.modelValue directly instead of emitting?Props are readonly in the child, so Vue warns in development and the assignment has no effect on the parent. The parent only updates when the child emits `update:modelValue`, which triggers the listener that `v-model` generated.
- Can a Vue 3 parent get v-model behaviour without writing v-model?Yes. Writing `:modelValue="x"` and `@update:modelValue="v => (x = v)"` by hand is exactly what the compiler generates. Parents do this when they want to transform or reject the value before assigning it.
- What did Vue 2 use for component v-model, and why does that matter when migrating?Vue 2 bound a `value` prop and listened for an `input` event. A migrated child that still emits `input` no longer updates a Vue 3 parent's `v-model`, because the compiler now listens for `update:modelValue`.
saying these in an interview costs you the question
- v-model on a component still uses a value prop and an input event in Vue 3.
- The child can assign to props.modelValue to update the parent.
- v-model on a component creates a shared reactive reference between parent and child.
- defineModel works without any prop or event being declared on the component.
- The parent must call a method on the child to read the updated value.