採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

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-namefirstName)

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: '...' }
  • requireddefault は同時には使わない(必須なら呼び出し側が必ず渡す/既定値があれば省略できる)。
  • テンプレートでは 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 … 省略時の値。requireddefault は排他(必須なら既定値は要らない)。
  • props は 読み取り専用。子コンポーネントの中で props.label = ... と書き換えてはいけない(変更は emit で親に依頼する → ch02-emits)。

なぜ「設定を外出し」が効くか

見た目(class)や中身(label)を props で外から注入する設計にすると:

  • 1 部品を再利用できる(ボタンの実装は 1 か所、使い方は無限)
  • 仕様変更(角丸を大きく等)が 1 ファイルの修正で全ボタンに反映される
  • テストが楽(props を変えて出力を確かめるだけ)

「汎用 UI 部品を作り、違いは props で吸収する」のは、実アプリ(Button / Card / Layout など)で

コンポーネントを設計する際の最も基本的な型。複雑なアプリほど、この小さな再利用部品の積み重ねでできている。