Course outline

Vue / Basic: reactive components and input contracts / Lesson 5

Props, events, slots, and component contracts

Build reusable components with clear input ownership and explicit proposals for change.

About 4 min readContent reviewed
On this page

Lesson goals

  • Use props as inputs and declared events as output contracts.
  • Explain parent scope, nested mutation, and reactive prop access.

Give a value one authoritative owner

A parent can pass a value through props while a child renders it and emits an event proposing a change. This makes ownership visible and avoids two independent copies that disagree. Props are readonly at the top-level child binding; mutating them directly violates the one-way flow. Nested objects and arrays can still expose mutable contents, so readonly props do not create deep isolation. Prefer narrow values and explicit update operations when shared mutation would be surprising. A reusable field should not require the entire application store merely to report that its text changed.

Declare the child’s public interface

defineProps and defineEmits are compiler macros in script setup and do not need runtime imports. Runtime prop declarations can document types and produce development warnings, but they are not a security validator for untrusted remote data. The example accepts a title and emits a string through change-title. It reads props.title without creating a private mutable duplicate. Native input value and input event form the underlying browser contract. In a typed component, narrow the event target appropriately rather than assuming every EventTarget has a value property. The JavaScript example leaves that TypeScript concern separate.

vue A child that proposes a title change
<script setup>
const props = defineProps({
  title: { type: String, required: true }
})
const emit = defineEmits(["change-title"])
</script>

<template>
  <label>
    Note title
    <input :value="props.title" @input="emit('change-title', $event.target.value)">
  </label>
</template>

Save as TitleField.vue in the stated Vue 3.5 SFC project. Typing emits the proposed string; the visible controlled value follows the prop supplied by the parent. The fragment does not persist or validate a remote record.

Close the parent value-event loop

The parent owns title and decides whether to accept the proposed event value. The example assigns it directly, while a real form may validate or normalize at the correct boundary. Component events do not bubble through arbitrary ancestors like native DOM events; forwarding must be deliberate. Naming an event declares an interface and can affect listener fallthrough behavior. Keep the child contract small enough to test with a supplied value and an event observer. A new parent value should update the field, which distinguishes a controlled input from a stale local copy initialized only once.

vue A parent owns the title and preview
<script setup>
import { ref } from "vue"
import TitleField from "./TitleField.vue"
const title = ref("Reading notes")
</script>

<template>
  <TitleField :title="title" @change-title="title = $event" />
  <p>Preview: {{ title }}</p>
</template>

This parent requires the previous TitleField.vue component and compatible compilation. The input and preview share the parent’s single state value. No component mounting or browser event test was executed for this fragment.

Respect the version-specific destructuring rule

In Vue 3.5, the SFC compiler keeps certain destructured defineProps bindings reactive by rewriting accesses in the same script setup context. In 3.4 and earlier, equivalent destructuring normally captured a value. This special transform does not make an argument passed to another function a reactive source: pass a getter or appropriate ref contract when the receiver needs future values. Accessing props.title explicitly is a clear portable pattern. Never infer that destructuring an arbitrary reactive object or store uses the same compiler rule. Review the actual source and API family before diagnosing a lost update.

Compose content without hiding data flow

Slots let a parent provide content while a child owns surrounding layout. Slot content normally uses the parent’s scope; scoped slots expose selected child data explicitly through slot props. This differs from passing a value as an ordinary prop and does not grant arbitrary access to a child’s internals. Fallthrough attributes and multiple root nodes also need deliberate forwarding when building reusable controls. Keep labels, disabled state, focus behavior, and emitted events consistent with the actual interactive element. A visual wrapper should preserve the browser and accessibility contract rather than burying it behind uncontrolled component state.

Common errors and fixes

A child edits a prop directly

Why it happens
Input ownership is confused with local mutable state.
How to fix it
Emit an explicit proposal and let the owner update the value.
Prevent it next time
Document one-way component contracts.

A copied prop never follows parent updates

Why it happens
Initialization created a disconnected local snapshot.
How to fix it
Read the prop or define a deliberate draft synchronization policy.
Prevent it next time
Test changing props after initial render.

An ancestor never receives a custom event

Why it happens
Component events were assumed to bubble like DOM events.
How to fix it
Forward the event or choose a clear shared owner.
Prevent it next time
Review each component boundary explicitly.

Check your understanding

1. A child receives a record object and changes record.title. Why can this surprise the parent?

Show worked solution

Top-level readonly props do not prevent every nested mutation. The child can affect shared data without the explicit event the parent expected. Prefer a narrow input and update event or a clearly documented shared-mutation contract.

2. A composable needs to follow a destructured prop in Vue 3.5. What should be passed?

Show worked solution

Pass a getter such as () => title, or another supported reactive-source contract, rather than the current primitive title value. The compiler’s local access transform does not turn a plain argument into a live source for an external function.

Key takeaways

  • Props supply values; events propose changes.
  • Reactive prop transforms are scoped and version-specific.
  • Slots and fallthrough need explicit ownership contracts.

Official references