Component v-model: Two-Way Binding with defineModel
v-model on a component creates a two-way binding, letting a parent and child share and update data. The `defineModel()` macro is the modern way to enable this for custom form inputs. The footgun is that a `default` value can de-sync parent/child state.
WHY IT EXISTS Components often need to modify data that lives in their parent, especially when creating custom form elements. The traditional method required manually declaring a prop to receive data and emitting an event to send updates back. This pattern was so common and verbose that Vue introduced a simpler, built-in solution.
THE MENTAL MODEL Think of v-model on a component as a direct, two-way data channel. The parent says, "Here's my data, you manage it," and the child can update that data directly. The defineModel() macro is the switch inside the child component that opens this channel, making the child behave like a native form input.
HOW IT WORKS defineModel() is a compiler macro that simplifies creating two-way bindings. Under the hood, it's syntactic sugar for the classic pattern. It automatically declares a prop (by default, modelValue) to receive the value from the parent. It also returns a ref that, when mutated, automatically emits an update event (by default, update:modelValue). The parent component's v-model directive listens for this specific event to update its local data. You can also pass options to defineModel() to make the prop required or give it a default value, just like a regular prop, for example: const model = defineModel({ required: true }).
WHEN TO USE IT Use defineModel() whenever you build a component that should behave like a form input. It is ideal for creating reusable, custom input components (like a styled text input or a complex date picker) that need to bind to a parent's data state. This keeps the parent component's template clean, using a familiar v-model directive.
WHEN NOT TO USE IT Avoid this pattern if a child component should not directly mutate parent state. If a component's job is only to display data, or if state changes should follow a more explicit one-way data flow (e.g., emitting a custom event with a payload for the parent to process), then passing a regular prop is more appropriate and predictable.
ONE CANONICAL EXAMPLE The most common use is wrapping a native input. In the child component (CustomInput.vue), you would have: <script setup> const model = defineModel() </script> <template> <input v-model="model" /> </template>. The parent can then use it just like a native input: <CustomInput v-model="searchText" />. When the user types in the custom input, the parent's searchText data property is automatically updated.
Read the original → vuejs.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.