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 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>—disabledistrue(present, no value) -
<MyInput>—disabledisfalse(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>
See One-Way Data Flow.
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)
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.