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>
  • defineProps and defineEmits are compiler macros — no import needed, and they must be called at the top level of the script block.
  • scoped styles 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 defineEmits and let the parent handle them, rather than reaching up through a chain of components.
  • defineModel('email') implements the v-model:email contract 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 / inject skips 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.

Reactivity and template syntax Components and props

Last refreshed 2026-09-18.