← All context files

vue3-options-api/props-events.md

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:

<!-- 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>