Vue — Props (defineProps)
テクノロジ系 / L02 component-design — コンポーネント設計
Vue — Props (defineProps)
基本
Greeting.vue:
<script setup lang="ts">
const props = defineProps<{
name: string;
age: number;
email?: string; // 省略可
}>();
</script>
<template>
<p>Hello, {{ props.name }} ({{ props.age }})</p>
<p v-if="props.email">Email: {{ props.email }}</p>
</template>defineProps<{...}>() で TS 型付け。
使う側
<script setup> import Greeting from "./Greeting.vue"; </script> <template> <Greeting name="Taro" :age="20" email="taro@example.com" /> </template>
- 文字列リテラルは
name="..." - それ以外は
:age="20"(v-bind) - 注意: kebab-case で書く慣習 (
first-name→firstName)
withDefaults
<script setup lang="ts">
interface Props {
name: string;
greeting?: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
greeting: "Hello",
count: 0,
});
</script>オプショナル props にデフォルト値を。
props を分割代入 (要 toRefs)
<script setup lang="ts">
import { toRefs } from "vue";
const props = defineProps<{ name: string; age: number }>();
const { name, age } = toRefs(props);
// → name: Ref<string>, age: Ref<number>
</script>そのまま const { name } = props すると リアクティブが切れる。
toRefs で個別 ref に分解する必要あり。
props は 読み取り専用
<script setup>
const props = defineProps<{ count: number }>();
props.count = 10; // ❌ 警告
</script>子は親の props を直接書き換えない。
変更したいなら親に通知 (emit) または local state にコピー。
complex オブジェクトの props
<script setup lang="ts">
interface User {
id: number;
name: string;
age: number;
}
const props = defineProps<{
user: User;
users: User[];
onAction?: (id: number) => void;
}>();
</script>interface を別 export して共有 (types/user.ts 等)。
v-bind="object" で一括渡し
<template>
<Greeting v-bind="userInfo" />
<!-- userInfo = { name: "Taro", age: 20 } -->
</template>object を展開して props として渡す。
まとめ
defineProps<{...}>()で TS 型付けwithDefaultsでデフォルト値- 分割代入は
toRefsで - props は 読み取り専用
- complex 型は interface 別出し
v-bind="object"で一括渡し
次は emits (子→親通知)。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — defineProps
問題
<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
name: string;
age: number;
email?: string;
}
const props = defineProps<Props>();
</script>
<template>
<div class="card">
<h2>{{ props.name }} ({{ props.age }})</h2>
<p v-if="props.email">Email: {{ props.email }}</p>
</div>
</template>
<!-- 親 -->
<UserCard name="Taro" :age="20" email="taro@example.com" />
<UserCard name="Hanako" :age="22" />合格基準
- 2人分カード表示
- email なしバージョンもOK
Lv2: 応用 — withDefaults
問題
<script setup lang="ts">
interface Props {
variant?: "primary" | "secondary";
size?: "small" | "medium" | "large";
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
size: "medium",
disabled: false,
});
</script>
<template>
<button :class="['btn', `btn-${props.variant}`, `btn-${props.size}`]" :disabled="props.disabled">
<slot />
</button>
</template>3パターンで使う:
<MyButton>Default</MyButton> <MyButton variant="secondary" size="small">Small</MyButton> <MyButton disabled>Disabled</MyButton>
合格基準
- デフォルト値が効く
- 3バリエーション表示
Lv3: 発展 — toRefs で分割
問題
props を分割代入する罠 + 修正。
<script setup lang="ts">
import { toRefs, watchEffect } from "vue";
const props = defineProps<{ name: string; age: number }>();
// ❌ リアクティブ切れる
const { name, age } = props;
watchEffect(() => console.log("bad:", name)); // 親が name 変えても発火しない
// ✅ toRefs で個別 ref に
const { name: nameRef, age: ageRef } = toRefs(props);
watchEffect(() => console.log("good:", nameRef.value)); // 発火する
</script>合格基準
- 両方試して、bad は発火しないことを確認
- toRefs の必要性を理解
提出
submission.md に3問の回答を。
ヒント
defineProps({ ... })でこのコンポーネントが受け取る props を宣言する。戻り値をpropsに代入する。- 必須の props は
{ type: String, required: true }、既定値つきは{ type: String, default: '...' }。 requiredとdefaultは同時には使わない(必須なら呼び出し側が必ず渡す/既定値があれば省略できる)。- テンプレートでは
props.labelのように使う。クラスの動的指定は:class="\btn-${props.variant}\"。
解説
props は「外から設定を注入する穴」
ボタンは画面のあちこちに出る。「送信」「削除」「キャンセル」…色も用途も違う。
これを毎回別コンポーネントで作るのは無駄。1 つの Button 部品を作り、違いは props で渡す。
const props = defineProps({
label: { type: String, required: true }, // 必須: 呼び出し側が必ず渡す
variant: { type: String, default: 'primary' },// 既定値つき: 省略できる
})呼び出し側:
<Button label="送信" /> <!-- primary(既定) --> <Button label="削除" variant="danger" /> <!-- danger -->
required: true… 渡し忘れると開発時に警告が出る(必須の明示)。default… 省略時の値。requiredとdefaultは排他(必須なら既定値は要らない)。- props は 読み取り専用。子コンポーネントの中で
props.label = ...と書き換えてはいけない(変更は emit で親に依頼する → ch02-emits)。
なぜ「設定を外出し」が効くか
見た目(class)や中身(label)を props で外から注入する設計にすると:
- 1 部品を再利用できる(ボタンの実装は 1 か所、使い方は無限)
- 仕様変更(角丸を大きく等)が 1 ファイルの修正で全ボタンに反映される
- テストが楽(props を変えて出力を確かめるだけ)
「汎用 UI 部品を作り、違いは props で吸収する」のは、実アプリ(Button / Card / Layout など)で
コンポーネントを設計する際の最も基本的な型。複雑なアプリほど、この小さな再利用部品の積み重ねでできている。