Vue 3 Options API – Props, Events, v-model
Declaring props
Always declare props as objects (not arrays) so you can specify type, default, required, and validators.
export default {
props: {
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
items: {
type: Array,
default: () => [], // function for non-primitive defaults
},
user: {
type: Object,
default: () => ({}),
},
status: {
type: String,
default: 'pending',
validator: (v) => ['pending', 'active', 'done'].includes(v),
},
},
}
Don't mutate props
Props are one-way down. Mutating a prop directly is a Vue warning and breaks parent/child sync.
// ❌
this.title = 'new'
// ✅ pattern: copy to local data, emit on change
data() {
return { localTitle: this.title }
},
watch: {
title(val) { this.localTitle = val }
},
methods: {
save() { this.$emit('update:title', this.localTitle) }
}
Emitting events
Declare emits explicitly — same reasons as declaring props.
export default {
emits: ['save', 'cancel', 'update:modelValue'],
methods: {
onSubmit() {
this.$emit('save', { id: this.id, name: this.name })
},
},
}
With validation:
emits: {
save: (payload) => payload && typeof payload.id === 'number',
cancel: null,
}
v-model on a custom component
Vue 3 changed the v-model contract:
- Default prop name:
modelValue(wasvaluein Vue 2) - Default event name:
update:modelValue(wasinput)
<!-- parent -->
<MyInput v-model="name" />
<!-- MyInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: {
modelValue: { type: String, default: '' },
},
emits: ['update:modelValue'],
}
</script>
Multiple v-model bindings
<UserForm v-model:first="user.first" v-model:last="user.last" />
export default {
props: {
first: String,
last: String,
},
emits: ['update:first', 'update:last'],
}
v-model modifiers
<MyInput v-model.trim="search" />
The component receives a modelModifiers prop:
props: {
modelValue: String,
modelModifiers: { default: () => ({}) },
},
methods: {
onInput(e) {
let val = e.target.value
if (this.modelModifiers.trim) val = val.trim()
this.$emit('update:modelValue', val)
},
}
Slots
<!-- Child.vue -->
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot :item="currentItem" /></main>
<footer><slot name="footer">default footer</slot></footer>
</div>
</template>
<!-- Parent -->
<Child>
<template #header>Title</template>
<template #default="{ item }">{{ item.name }}</template>
</Child>