こんにちは、かつコーチです。
前回は watch と watchEffect の使い分けを解説しました。
今回は、コンポーネント同士でデータをやり取りするための基本、「Props(プロップス)」を扱います。
Vueでの開発は、画面を小さなコンポーネントに分けて組み合わせていくスタイルが基本です。
そのときに欠かせないのが、親から子へデータを渡す仕組みであるPropsです。
Propsとは?
なぜPropsが必要なのか
Props とは、親コンポーネントから子コンポーネントへ渡すデータのことです。
たとえば「ユーザーカード」を表示するコンポーネントを考えてみましょう。
名前や年齢が違う複数のユーザーを表示したい場合、コンポーネントの中身は同じでも、表示するデータだけを変えたいはずです。
このとき、親から「このデータを使って表示して」と渡す役割を果たすのがPropsです。
コンポーネントを再利用可能な「型」として設計し、中身のデータだけを外から差し替える、という考え方だとイメージすると分かりやすいです。
親から子への一方通行
Propsの重要な性質として、データは親から子への一方通行という点があります。
子コンポーネント側でPropsの値を直接書き換えることはできません。
「子から親にデータを伝えたい」という逆方向の通信には、次回解説する emit という別の仕組みを使います。
まずは「Propsは親から子への一方通行」というルールをしっかり覚えておきましょう。
Propsの基本の書き方
子コンポーネント側で受け取る
子コンポーネント(UserCard.vue を想定)では、defineProps を使ってPropsを定義します。
<script setup lang="ts">
interface Props {
name: string
age: number
}
const props = defineProps<Props>()
</script>
<template>
<div class="user-card">
<p>{{ props.name }}さん({{ props.age }}歳)</p>
</div>
</template>
<script setup> では interface で型を定義し、defineProps<Props>() とすることで、TypeScriptの型チェックが効いたPropsを受け取れます。
defineProps はimport不要で使える特別な関数(コンパイラマクロ)なので、そのまま使って問題ありません。
親コンポーネント側で渡す
親コンポーネント側では、属性のようにPropsを渡します。
<script setup lang="ts">
import UserCard from './UserCard.vue'
</script>
<template>
<UserCard name="かつコーチ" :age="30" />
</template>
文字列をそのまま渡す場合は name="かつコーチ" のように書けますが、数値や変数、オブジェクトを渡す場合は :age="30" のようにコロン(v-bind の省略形)を付ける必要があります。
私が初心者の頃、この「コロンを付ける・付けない」の判断でよくつまずいていました。
つまずきやすいポイント:コロンの付け忘れ
数値が文字列として渡ってしまう
❌ Before:コロンを付けずに渡してしまう
<template>
<UserCard name="かつコーチ" age="30" />
</template>
<script setup lang="ts">
interface Props {
name: string
age: number
}
const props = defineProps<Props>()
// props.age は本来number型のはずが、実際には文字列の"30"として渡ってきてしまう
</script>
このコードは一見動いているように見えますが、age にはコロンを付けていないため、常に文字列("30")として渡されてしまいます。
TypeScriptで型チェックが効いていれば警告に気づけますが、そうでない場合は「なぜ計算結果がおかしいのか」と気づきにくいバグにつながります。
私自身、年齢に1を足す処理を書いたときに "30"+1 が "301" になってしまい、しばらく原因が分からなかった経験があります。
✅ After:コロンを付けて動的な値として渡す
<template>
<UserCard name="かつコーチ" :age="30" />
</template>
コロンを付けることで、30 はJavaScriptの式として評価され、正しくnumber型の値として渡されます。
「固定の文字列を渡すとき以外は、基本的にコロンを付ける」と覚えておくと、この手の事故を防げます。
Propsに初期値や制約を付ける
デフォルト値を設定する
Propsが渡されなかった場合の初期値を設定したいときは withDefaults を使います。
<script setup lang="ts">
interface Props {
name: string
age?: number
}
const props = withDefaults(defineProps<Props>(), {
age: 20,
})
</script>
<template>
<p>{{ props.name }}さん({{ props.age }}歳)</p>
</template>
age?: number のように ? を付けて任意(オプショナル)にしたうえで、withDefaults の第2引数にデフォルト値を指定します。
こうしておくと、親コンポーネントが age を渡さなかった場合でも 20 が使われるため、undefined によるエラーを防げます。
分割代入で受け取るときの注意
Vue 3.5以降では、defineProps の戻り値を分割代入で受け取ることもできます。
<script setup lang="ts">
interface Props {
name: string
age?: number
}
const { name, age = 18 } = defineProps<Props>()
</script>
<template>
<p>{{ name }}さん({{ age }}歳)</p>
</template>
分割代入でもデフォルト値を指定できるため、withDefaults を書かずに済むケースが増えます。
どちらの書き方も現場で見かけるので、両方を知っておくとコードリーディングに困りません。
まとめ
この記事のポイント
- Propsは親コンポーネントから子コンポーネントへデータを渡す仕組みで、一方通行である
- 子コンポーネント側では
defineProps<Props>()で型付きのPropsを受け取る - 動的な値を渡すときはコロン(
:)を付け忘れると、文字列として渡ってしまう withDefaultsや分割代入でPropsにデフォルト値を設定できる
次に読むべき記事
次回は、Propsとは逆方向、子コンポーネントから親コンポーネントへイベントを伝える emit を解説します。
→ 次の記事:emitで子から親にイベントを伝える