skip to content

When upgrading a Vue 2 component used with `v-model` and `:title.sync`, what must change in the child and in the parent for Vue 3?

level: middleimportance: must knowfreq 70%

answer

  1. the default prop and event renamed
  2. the model option is gone
  3. arguments on v-model replace .sync
  4. several v-models on one component

basics

~10 s

Vue 3's component v-model uses the modelValue prop and update:modelValue event instead of value and input. The model option and .sync are removed: parents write v-model:title, which binds title and listens for update:title.

solid answer

~40 s

In Vue 2, `v-model` on a component meant `:value` plus `@input`, customisable with the `model: { prop, event }` option, and extra two-way props used `:title.sync`, sugar for `:title` plus `@update:title`. Vue 3 unifies these. Plain `v-model` now means `:modelValue` plus `@update:modelValue`, so the child renames its `value` prop to `modelValue` and emits `update:modelValue` instead of `input`. The `model` option and `.sync` are removed; both become an **argument**: `v-model:title="pageTitle"` binds `title` and listens for `update:title`. A child that already emitted `update:title` for `.sync` needs no change; only the parent's template does. Because arguments work, one component can take several `v-model`s. Declare the update events in `emits`. `v-model` on native inputs is unchanged.

code

vue · 12 lines
vue
<!-- TitleInput.vue, Vue 3 (was: props: ['value'] + this.$emit('input', v)) -->
<script setup lang="ts">
defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
</script>

<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
  />
</template>

go deeper

for a junior

Remember the new names: modelValue and update:modelValue for plain v-model, and v-model:title where Vue 2 used :title.sync.

for a middle

Explain how value/input, the model option and .sync collapsed into one prop plus update:prop pattern, and why that allows several v-models per component.

for a senior

Plan the edit so child and call sites change together, and recognise the half-migrated symptoms: an empty field from an undeclared modelValue, or .sync silently going one-way.

for a principal

For a shared component library, treat v-model names as public API: version the change, migrate consumers in lockstep, and use the migration build to find stragglers.

## Vue 2's three mechanisms Vue 2 had three overlapping ways to express two-way binding on a component: - **`v-model`** was hard-wired to a `value` prop and an `input` event: `<Child v-model="t" />` meant `<Child :value="t" @input="t = $event" />`. - **The `model` option** (added in 2.2) let a child rename that pair, for example `model: { prop: 'checked', event: 'change' }`, but still allowed only one `v-model`. - **`.sync`** covered every other prop: `<Child :title.sync="t" />` meant `:title` plus `@update:title`, and the child called `this.$emit('update:title', newValue)`. ## Vue 3's single mechanism Vue 3 standardises on one pattern, `prop` + `update:prop`: - **Plain `v-model`** means `:modelValue` plus `@update:modelValue`. - **`v-model:title`** (an argument) means `:title` plus `@update:title`, replacing both `.sync` and the `model` option. - **Several `v-model`s** on one component are allowed: `<Child v-model:title="t" v-model:content="c" />`. - **Custom modifiers** such as `v-model.capitalize` became possible on components. | Vue 2 | Vue 3 | |---|---| | `v-model` = `:value` + `@input` | `v-model` = `:modelValue` + `@update:modelValue` | | `model: { prop: 'checked', event: 'change' }` | Removed: parent writes `v-model:checked`, child emits `update:checked` | | `:title.sync="t"` | `v-model:title="t"` | | One `v-model` per component | Any number, one per argument | ## What to change, side by side ```html <!-- Parent, Vue 2 --> <TitleInput v-model="title" /> <PageMeta :title.sync="pageTitle" /> <!-- Parent, Vue 3 --> <TitleInput v-model="title" /> <PageMeta v-model:title="pageTitle" /> ``` 1. **Children used with plain `v-model`:** rename the `value` prop to `modelValue` and emit `update:modelValue` instead of `input`. The parent template does not change. 2. **Children with a `model` option:** delete the option; keep the prop (say `checked`), emit `update:checked`, and change parents to `v-model:checked`. 3. **`.sync` call sites:** change `:title.sync` to `v-model:title` in the parent. The child already emits `update:title`, so it usually needs nothing. 4. **Declare the events** in `emits` (or `defineEmits`) so they are documented and not treated as fallthrough listeners. ## What breaks if only one side is migrated - **Parent on Vue 3 syntax, child still on `value`/`input`:** the child receives a `modelValue` it does not declare, which lands in `$attrs` and by default falls through to its root element, while its own `value` prop is `undefined`. Its `$emit('input', ...)` reaches no listener, so the field renders empty and typing never updates the parent. - **`.sync` left in a Vue 3 template:** the modifier is no longer recognised, so no `update:title` listener is generated. The binding silently becomes one-way: the child emits, nobody updates `pageTitle`. - **Native elements are unaffected.** `v-model` on `<input>`, `<select>` and `<textarea>` still binds the element's value and listens for `input` (or `change` with `.lazy`); the rename only concerns components. ## Why Vue 3 made the change Vue 2's design had three problems that the new convention solves at once: - **`value` was taken.** Hard-wiring `v-model` to a `value` prop meant a component could not use `value` for anything else without the `model` option, and the name blurred the line between native elements and components. - **Only one binding.** A form component that needed two-way `title` and `content` had to mix `v-model` with `.sync`, two syntaxes for one idea. - **Two ways to do one thing.** `.sync` and `model` were separate features that both ended in `prop` + event. One `prop` + `update:prop` convention, with an optional argument, covers all three cases and is what the template compiler now generates for every `v-model` on a component. ## During a gradual upgrade The migration build tracks these as the `COMPONENT_V_MODEL` and `COMPILER_V_BIND_SYNC` flags, and its warning for the former tells you to use the `modelValue` prop and `update:modelValue` event. A library shared by many apps is safest migrated component by component, changing each child and every call site of it in the same step.

  • A Vue 2 child already emits `update:title` for a parent using `:title.sync`. What changes in the child when you move to Vue 3?
    Usually nothing in its logic. `.sync` was sugar for `:title` plus `@update:title`, and Vue 3's `v-model:title` expands to exactly the same pair, so the child keeps its `title` prop and its `update:title` emit. Only the parent changes `:title.sync` to `v-model:title`. It is still worth declaring `update:title` in `emits`.
  • Why did Vue 3 rename the default prop to `modelValue` instead of keeping `value`?
    Hard-wiring `value` to `v-model` meant a component could not use a prop named `value` for anything else without the `model` option, and it blurred the line between native elements and components. A dedicated `modelValue` name frees `value` for ordinary use and fits the single `prop` + `update:prop` convention that also covers every `v-model:arg`.

saying these in an interview costs you the question

  • Vue 3 component v-model still passes value and listens for input
  • The model option still renames Vue 3's v-model prop
  • .sync still works in Vue 3 as a shortcut for v-model
  • A component can have only one v-model in Vue 3
  • Native input v-model also switched to modelValue in Vue 3