Registration and Props

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Before a component can appear in a template it must be registered so the compiler can resolve its tag. Once registered, it receives data from its parent through props — a one-way channel that this page covers end to end, including the ways to derive editable local state from a prop without breaking that channel.

Global vs. local registration

Global registration makes a component available in every template of the app, with no import:

import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'

const app = createApp(App)
app.component('BaseButton', BaseButton)   // usable as <BaseButton> anywhere
app.mount('#app')

app.component() is chainable. The trade-off: globally registered components cannot be tree-shaken, so one used on a single screen still ships in the initial bundle, and the dependency is invisible at the use site.

Local registration — the default with <script setup> — exposes a component to the current component only. Anything imported into a <script setup> block is automatically available in that component’s template:

<script setup>
import BaseButton from './BaseButton.vue'
</script>

<template>
  <BaseButton>Save</BaseButton>
</template>

Local registration keeps the dependency graph explicit and tree-shakeable; reach for global registration only for a small set of truly ubiquitous base components. See Component Registration.

Component name casing

Author component names in PascalCase. In single-file component templates Vue also resolves the kebab-case form, so <BaseButton> and <base-button> refer to the same component; PascalCase in templates is preferred because it visually separates components from native elements. The name is inferred from the file name and is used by <KeepAlive>, devtools, and recursive self-reference. See Component name casing.

Declaring props

defineProps() is a compile-time macro — no import, <script setup> only. It has two mutually exclusive forms.

Runtime declaration passes an array of names or an object of options:

<script setup>
const props = defineProps({
  title: String,
  likes: Number
})
</script>

<template>
  <h3>{{ props.title }} -- {{ props.likes }}</h3>
</template>

Type-based declaration passes a type argument and lets Vue derive the runtime checks from it. This is the idiomatic form in TypeScript:

const props = defineProps<{
  title: string
  likes?: number
}>()

Props are exposed on the template directly (\{\{ title }}) and, in script, through the object defineProps() returns. That object is reactive and read-only.

Prop validation

The object form doubles as a validation schema. Each entry may specify type, required, default, and a validator function:

defineProps({
  status: {
    type: String,
    required: true,
    validator: (value) => ['active', 'paused', 'archived'].includes(value)
  },
  // `default` supplies the value when the prop is absent or `undefined`
  size: {
    type: String,
    default: 'medium'
  },
  // multiple allowed types
  id: [String, Number],
  // a custom class as the type -- checked with instanceof
  parsedAt: Date
})

Validation failures produce a console warning in development only and never block rendering. With type-based declaration the same constraints come from the type, and default / required come from reactive props destructure or withDefaults() (below). See Prop Validation.

Boolean casting

A prop whose type list includes Boolean follows the same casting rules as native boolean attributes. Given defineProps(\{ disabled: Boolean }):

  • <MyInput disabled> — disabled is true (present, no value)

  • <MyInput> — disabled is false (absent)

  • <MyInput :disabled="false"> — false

With [Boolean, String] the presence-implies-true rule still applies because Boolean comes first; swap to [String, Boolean] and an empty value is kept as the empty string. See Boolean Casting.

Object and array defaults

Objects and arrays are reference types, so their default must be returned from a factory function — every instance then gets its own copy instead of sharing one:

defineProps({
  tags: {
    type: Array,
    default: () => []
  },
  config: {
    type: Object,
    default: () => ({ retries: 3 })
  }
})

With type-based declaration, withDefaults() wraps defineProps() and takes the same factories:

withDefaults(defineProps<{
  tags?: string[]
  config?: { retries: number }
}>(), {
  tags: () => [],
  config: () => ({ retries: 3 })
})

One-way data flow

Props are a one-way-down binding: a change in the parent flows to the child, never the reverse. Each update in the parent refreshes the child’s props, so a child that mutated a prop would see that change silently overwritten — and mutating a prop also breaks single-source-of-truth. Vue warns when you assign to one. React enforces the same discipline (React Reference) and Angular models it with input() / output() (Angular Reference).

<script setup>
const props = defineProps({ initialCount: Number })

// props.initialCount++   // warning: "Set operation on key failed: target is readonly"
</script>

Turning a prop into editable state

Three cases justify local state derived from a prop, each with a canonical pattern.

The prop is only an initial value; the child owns it afterwards. Copy it into a ref once:

<script setup>
import { ref } from 'vue'

const props = defineProps({ initialCount: Number })
const count = ref(props.initialCount)   // later prop updates are ignored, by design
</script>

The prop needs local transformation. Use a writable computed with a getter and setter:

<script setup>
import { computed } from 'vue'

const props = defineProps({ size: String })
const emit = defineEmits(['update:size'])

const normalized = computed({
  get: () => props.size.trim().toLowerCase(),
  set: (value) => emit('update:size', value)
})
</script>

The child should write straight back to the parent’s binding. That is two-way binding — use defineModel(), covered in Component Events and v-model:

<script setup>
const model = defineModel()   // parent writes <Child v-model="..." />
model.value = 'next'          // updates the parent, no explicit emit
</script>

Reactive props destructure

Since Vue 3.5, destructuring the return of defineProps() stays reactive: the compiler rewrites each reference to a destructured variable back into a props.x access, and a default can be given with plain JavaScript default-value syntax.

const { title, likes = 0 } = defineProps<{
  title: string
  likes?: number
}>()

watchEffect(() => console.log(title, likes))   // re-runs when either prop changes

The catch: passing a destructured prop straight into a function or composable hands over its current value, not a reactive source. See Reactive Props Destructure.

Passing props into composables

To feed a prop into a composable or a watch source without losing reactivity, wrap it. toRef() turns one prop into a ref; toRefs() converts every prop into a set of refs at once:

import { toRef, toRefs } from 'vue'

const props = defineProps<{ userId: string; expanded: boolean }>()

// one prop as a ref
const userId = toRef(props, 'userId')
useUser(userId)

// a getter also stays reactive
useUser(() => props.userId)

// all props at once
const { userId: id, expanded } = toRefs(props)

A ref produced by toRef from a read-only prop is itself read-only. See toRef() and toRefs().

See also

  • Component Events and v-model — defineEmits(), defineModel(), and fallthrough attributes.

  • Slots — the other channel for passing content from parent to child.

  • Provide / Inject — getting data past intermediate components without prop drilling.

  • Composables — packaging the reactive logic that props feed into.