Vue docs, chapter by chapter

Components I — registration, props, events, v-model

Component registration, props declaration and validation including Boolean casting and reactive destructure, event declaration and validation, and component v-model with defineModel.

The Components In-Depth chapters are the ones interviewers quote when they want to know whether you learned Vue or only used it. Prop validation options, the Boolean casting rule, what declaring an emit changes, and how v-model desugars on a component are all checkable facts with precise answers.

Know — Registration

  • Global app.component('MyComponent', MyComponent): usable anywhere, but not tree-shaken and dependencies become implicit. Local: import in <script setup> (usable directly) or list in the components option.
  • Naming: PascalCase in SFC templates (<MyComponent>); in-DOM templates need kebab-case (<my-component>). PascalCase registration lets both forms work.

Know — Props

  • Declaration: defineProps(['foo']), an object with types and options, or a TypeScript type argument defineProps<{ foo: string }>(). Declared props are removed from $attrs.
  • Reactive props destructure (3.5+): const { foo = 'x' } = defineProps<...>() stays reactive because the compiler rewrites foo to props.foo; pass it to watch as a getter, and to composables as () => foo or through toValue. Before 3.5 destructuring loses reactivity, and defaults use withDefaults.
  • Casing: declare camelCase, pass kebab-case in templates (greeting-message). Static and dynamic (:title) values; numbers, booleans, arrays and objects need : even when literal. v-bind="post" passes every property of an object as props.
  • One-way data flow: a child must not mutate a prop; copy it into local state or derive a computed. Mutating nested objects or arrays inside a prop is possible and discouraged because it hides the source of the change.
  • Validation (object syntax): type (native constructor, custom class, array of types, or null/undefined for any), required, default (function for objects and arrays; functions as values need default: () => fn), validator(value, props). Absent optional props are undefined except Boolean; default applies to undefined. Type checks happen in development only.
  • Boolean casting: an absent Boolean prop is false; a present prop with no value (<Comp disabled>) is true; with mixed [Boolean, String] the order decides how "" is cast (Boolean first means "" becomes true).
  • Runtime type checks use instanceof for custom classes and Object.prototype.toString for natives.

Know — Events

  • Emitting: $emit('name', ...args) in templates, emit from defineEmits in setup. Arguments after the name are passed to the listener; an inline listener receives them as $event only for the first one, otherwise use an arrow function.
  • Declaring emits (defineEmits(['inFocus']), object syntax with validators, or the TypeScript call-signature or named tuple syntax defineEmits<{ change: [id: number] }>() 3.3+): declared events are documented, validated and excluded from $attrs, so an @click on the component does not also attach a native listener to the root.
  • Casing: emit camelCase, listen kebab-case (@some-event). Events do not bubble; a grandparent needs a chain of emits, a provide/inject callback, or a store.
  • Validation: with the object syntax each key is a function returning a boolean; failures warn in development.

Know — Component v-model

  • defineModel() (3.4+) returns a ref bound to the parent's v-model; it declares a modelValue prop and an update:modelValue event under the hood. Options: required, default, type, and get/set transformers.
  • Arguments: v-model:title on the parent pairs with defineModel('title'); several v-models are allowed on one component.
  • Modifiers: const [model, modifiers] = defineModel() exposes them; a set option can apply them (capitalize example in the guide). With arguments, modifiers arrive as titleModifiers.
  • Pre-3.4 pattern: modelValue prop + update:modelValue emit, either bound directly on the native input (:value + @input="$emit('update:modelValue', $event.target.value)") or through a writable computed.

How it works

app/components/Docs/PriceInput.vue
<script setup lang="ts">
// Reactive props destructure with a default (3.5+), Boolean casting, custom validator
const { currency = 'EUR', compact } = defineProps<{
  currency?: string
  compact?: boolean // absent -> false, `<PriceInput compact>` -> true
}>()

// Named tuple syntax (3.3+); declared events are not passed through as native listeners.
const emit = defineEmits<{
  blur: []
  change: [amount: number, currency: string]
}>()

// v-model:amount with a `.cents` modifier, normalised in the setter
const [amount, modifiers] = defineModel<number>('amount', {
  default: 0,
  set: v => (modifiers.cents ? Math.round(v * 100) / 100 : v),
})

function onInput(e: Event) {
  amount.value = Number((e.target as HTMLInputElement).value)
  emit('change', amount.value, currency)
}
</script>

<template>
  <input :value="amount" type="number" :class="{ compact }" @input="onInput" @blur="emit('blur')">
</template>
usage
<PriceInput v-model:amount.cents="price" currency="CZK" compact @change="save" />

Runtime validation in the object syntax, for the facts that only exist there:

defineProps({
  size: { type: String, default: 'md', validator: (v: string) => ['sm', 'md', 'lg'].includes(v) },
  items: { type: Array, default: () => [] },  // factory: a shared array would leak between instances
  author: Person,                              // custom class, checked with instanceof
  meta: { type: [Object, null], required: true }, // null allows any, here "object or null"
})
Gotcha· A default object without a factory

default: [] would share one array across every instance of the component (the same object reference), so mutating it in one instance changes it everywhere. Objects and arrays need default: () => []. Functions used as values need a wrapper too, otherwise Vue calls them as the factory.

Exercise

Exercise
  • Declare a [Boolean, String] prop and a [String, Boolean] prop, pass <Comp flag> to both, and compare the received values.
  • Add @click to a component that declares emits: ['click'] and to one that does not; check which one fires a native click from the root element.
  • Rewrite a pre-3.4 modelValue/update:modelValue component with defineModel, keeping a .trim modifier working.

Be able to say

Be able to say· What does declaring emits actually change?

"Three things. It documents the component's events and, with the object or TypeScript syntax, validates their payloads in development. It removes those events from $attrs, so a listener for a declared event is not also attached as a native listener on the root element, which prevents double handling for names like click or submit. And it makes the events part of the component's typed contract, which is what IDEs and vue-tsc check at the call site. Without declaration a listener falls through as an attribute and fires as a native event, which is the source of a classic 'my handler ran twice' bug."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.