Components, props and events
Single-file components, typed props, emitted events, slots and provide/inject — the seams between one component and the next.
Anatomy of a single-file component
A .vue file has three optional blocks: a script block with setup semantics, a template, and scoped styles. The script block runs once per instance, before the component first renders.
// Button.vue — script block
import { ref, defineProps, defineEmits } from 'vue'
const props = defineProps({
label: { type: String, required: true },
kind: { type: String, default: 'primary' }, // primary | ghost
busy: { type: Boolean, default: false }
})
const emit = defineEmits(['submit', 'cancel'])
const clicks = ref(0)
function onClick() {
clicks.value += 1
emit('submit', { label: props.label })
}<!-- Button.vue — template block -->
<button
:class="['btn', 'btn-' + kind]"
:disabled="busy"
@click="onClick"
>
{{ busy ? 'Working...' : label }}
</button>
<style scoped>
.btn { border-radius: 6px; }
.btn-ghost { background: transparent; }
</style>definePropsanddefineEmitsare compiler macros — no import needed, and they must be called at the top level of the script block.scopedstyles are rewritten with a per-component attribute, so a parent cannot restyle the child's internals by accident.- Declare props with types and defaults even in plain JavaScript: the type check is a runtime warning, and the declaration doubles as documentation.
Props down, events up
<Button
label="Save"
:busy="saving"
@submit="onSave"
@cancel="saving = false"
/>
<ChildForm v-model:email="email" /> <!-- v-model on a custom component -->- Props are one-way: a child may not assign to
props.x. Copy into local state (const draft = ref(props.value)) when the child needs to edit. - Custom events do not bubble. Declare them with
defineEmitsand let the parent handle them, rather than reaching up through a chain of components. defineModel('email')implements thev-model:emailcontract in the child without hand-written prop-plus-event plumbing.- Slots pass markup down; named slots (
#header) and scoped slots (#row="{ item }") let the parent control rendering while the child controls layout. provide/injectskips intermediate levels for genuinely shared context such as theme or an i18n instance — not for ordinary data flow.
Lifecycle and composables
import { onMounted, onUnmounted, useTemplateRef } from 'vue'
const canvas = useTemplateRef('canvas') // matches ref="canvas" in the template
onMounted(() => {
draw(canvas.value)
window.addEventListener('resize', onResize)
})
onUnmounted(() => window.removeEventListener('resize', onResize))// useCounter.js — shared logic, no component wrapper
import { ref, onUnmounted } from 'vue'
export function useCounter(interval = 1000) {
const n = ref(0)
const id = setInterval(() => n.value++, interval)
onUnmounted(() => clearInterval(id)) // works because it runs in a component scope
return { n }
}💡
Composables are the replacement for mixins: a plain function that uses reactivity and returns what the caller needs. Extract one when the same three or four lines of stateful logic appear in a second component.
FAQ
Why is my prop undefined on first render?
Either the prop was not declared in
defineProps (attributes then land in the fallthrough set instead), or the parent bound a value that is still loading. Render a loading state rather than assuming the data exists.When should I use a slot instead of a prop?
A prop carries data; a slot carries markup. If the parent needs to pass elements, components or multiple differently styled fragments, use a slot.
Related
Reactivity and template syntax Components and props
Last refreshed 2026-09-18.