Passing Data In

Declare a component's inputs with defineProps, read them in the template and in script, and understand why a prop is read-only.

Syntaxconst props = defineProps<{ title: string; unreadCount?: number }>()

A prop is a named input that a parent passes into a component. Props are what make a component reusable: one UserCard renders any user.

Declaring them

Declare props with defineProps inside <script setup>. It is a compile-time macro, so it needs no import, and in TypeScript you pass the shape as a type argument. Declared props are available in the template by name. In script you read them through the object defineProps returns, as props.name.

Casing and shorthand

Declare in camelCase and pass in kebab-case: unreadCount is written :unread-count="3". :unread-count is shorthand for v-bind:unread-count and they are the same directive. Since Vue 3.4 there is a same-name shorthand too: :title with no value means :title="title".

One way only

Props are read-only. Data flows down from the parent, and the child cannot assign to a prop; the development build logs a warning if you try. When you need a value the child can change, copy the prop into a ref and treat it as a starting value. When you need a changed version, derive it with computed.

What trips people up

Read-only applies to the binding, not to the contents. props.user.name = 'Ada' raises no warning and quietly edits the parent's object, because objects are passed by reference.

Example

Example · vue
<!-- src/components/UserCard.vue -->
<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{
  name: string
  email: string
  unreadCount?: number
}>()

const initials = computed(() =>
  props.name
    .split(' ')
    .map((part) => part.charAt(0))
    .join('')
    .toUpperCase()
)
</script>

<template>
  <article>
    <p>{{ initials }}</p>
    <h4>{{ name }}</h4>
    <p>{{ email }}</p>
    <p v-if="unreadCount">{{ unreadCount }} unread</p>
  </article>
</template>

<style scoped>
article {
  border: 1px solid #d0d0d0;
  padding: 1rem;
}
</style>

When to use it

  • Render the same card component for every user in a list.
  • Make a button component accept a label and a disabled flag.
  • Size or theme a component from the page that uses it, without editing the component.

More examples

Passing the props from a parent

All three forms pass identical props. Note :unread-count="3" rather than unread-count="3": with the colon the value is a JavaScript expression and arrives as the number 3, and without it the value is a plain attribute string and arrives as "3", which fails type checking. The same-name shorthand on the third card only works when the local variable and the prop share a name, and it arrived in Vue 3.4.

Example · vue
<!-- src/views/TeamPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import UserCard from '../components/UserCard.vue'

const name = ref('Ada Lovelace')
const email = ref('[email protected]')
</script>

<template>
  <!-- longhand -->
  <user-card
    v-bind:name="name"
    v-bind:email="email"
    v-bind:unread-count="3"
  />

  <!-- the same three bindings, shorthand -->
  <user-card :name="name" :email="email" :unread-count="3" />

  <!-- same-name shorthand, Vue 3.4+: :name means :name="name" -->
  <user-card :name :email :unread-count="3" />
</template>

Default values, two ways

Two ways to give an optional prop a default. The destructuring form shown here uses ordinary JavaScript defaults, and from Vue 3.5 the destructured names stay reactive because the compiler rewrites them back to props.tone. The longhand is withDefaults(defineProps<Props>(), { tone: 'neutral' }), which does the same thing and is what you must use on Vue 3.4 and earlier. If you pass a destructured prop to a function that tracks it, wrap it in a getter: watch(() => tone, handler), not watch(tone, handler).

Example · vue
<!-- src/components/StatusBadge.vue -->
<script setup lang="ts">
// Destructured defaults. Destructured props stay reactive from Vue 3.5.
const { label, tone = 'neutral' } = defineProps<{
  label: string
  tone?: 'neutral' | 'warning'
}>()
</script>

<template>
  <span :data-tone="tone">{{ label }}</span>
</template>

<style scoped>
span[data-tone='warning'] {
  color: #8a4b00;
}
</style>

Deriving from a prop instead of writing to it

The two patterns that replace writing to a prop. ref(props.initialText) copies the value at setup time, so the component owns something editable; note that later changes to the prop will not reach it, which is why the prop is named initialText. computed derives a value that does follow its source. Reach for the ref copy for editable form fields, and for computed whenever you only need the prop in a different shape.

Example · vue
<!-- src/components/DraftEditor.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue'

const props = defineProps<{
  initialText: string
}>()

// A local copy the component may change. The prop seeds it once.
const text = ref(props.initialText)

// A derived value. Recomputes whenever text changes.
const remaining = computed(() => 280 - text.value.length)
</script>

<template>
  <div>
    <textarea v-model="text"></textarea>
    <p>{{ remaining }} characters left</p>
  </div>
</template>

<style scoped>
textarea {
  width: 100%;
}
</style>

Discussion

  • Be the first to comment on this lesson.