Vue.js: コンポーネント通信

最終更新:2026-10-20

コンポーネント間の通信は、Vueの中核となる仕組みです。親コンポーネントはpropsを介して子コンポーネントにデータを渡し、子コンポーネントはemitsを使用してイベントをトリガーし、親コンポーネントに通知します。双方向のデータフローを理解することは、優れたVueアプリケーションを構築するための鍵となります。

Vue 3の<script setup>は、definePropsとdefineEmitsという2つのコンパイル時マクロを提供しており、これらはインポートせずに使用可能で、TypeScriptの型推論を完全にサポートしています。

関連テンプレートとヘルパー:useAttrs() v-model

1. 学習内容


2. Eコマースのショッピングカートにおける「双方向バインディング」の落とし穴

(1) 課題:子コンポーネントがプロパティを変更すると、親コンポーネントがエラーをスローしてしまう

アリスは、数量を入力できるカートを実装しました:

Vue でエラーが発生します:

[Vue warn] Set operation on key "quantity" failed: target is readonly.

1時間デバッグを行った結果、チャーリーは次のことを発見した:

「アリス、数量を変更しても親コンポーネントのカート合計が更新されないんだ。Vueはプロパティを変更しているのに、親コンポーネントがそれを認識していないみたい。」

(2) Vueの一方向データフロー + emit の解決策

これで変更が正しく反映されるようになりました:子ノードが変更を送信 → 親ノードが受信 → 親ノードがデータを更新 → 子ノードへ再送信。

(3) 収益

データフローを修正した後:


3. props の完全な構文

(1) 7種類のプロップに関する宣言

VUE
<script setup>
defineProps({
  // 1. Basic Types
  name: String,
  age: Number,
  active: Boolean,
  
  // 2. Complex Types
  user: Object,
  items: Array,
  
  // 3. Multiple Types(Any)
  id: [String, Number],
  
  // 4. Required
  title: { type: String, required: true },
  
  // 5. Default value
  pageSize: { type: Number, default: 20 },
  
  // 6. Custom Validation
  email: {
    type: String,
    validator: (val) => val.includes('@')
  },
  
  // 7. Function Default Values(The default value must be a function.)|
  createdAt: {
    type: Date,
    default: () => new Date()
  }
})
</script>

(2) 詳細な例

VUE
<script setup>
defineProps({
  // String
  title: String,
  
  // Numbers + Default value + Verification
  pageSize: {
    type: Number,
    default: 20,
    validator: (val) => val > 0 && val <= 100
  },
  
  // Boolean(Note:The default value for falsy Values should be in function form)
  isVip: {
    type: Boolean,
    default: false
  },
  
  // Array Default Values(It must be returned as a function)
  tags: {
    type: Array,
    default: () => ['vue', 'javascript']
  },
  
  // Object Default Values(It must be returned as a function)
  user: {
    type: Object,
    default: () => ({ name: 'Guest', age: 0 })
  },
  
  // Required + Custom Validation
  email: {
    type: String,
    required: true,
    validator: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
  }
})
</script>

(3) 6つのデフォルト値に関するルール

ルール 例
基本型 default: 20
配列/オブジェクト default: () => []
null default: null
未定義 デフォルトは不要
関数リファレンス default: fn は使用せず、() => fn() を使用してください
Symbol / BigInt ファクトリ関数が返す値

4. TypeScript の強タイププロップ(Vue 3 での使用を推奨)

(1) プリミティブ型の宣言

VUE
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

defineProps<{
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}>()
</script>

(2) 必須 vs. 任意 (?)

TS
defineProps<{
  // Required(Default)
  user: User
  pageSize: number
  
  // Optional (add ?)
  variant?: 'primary' | 'secondary'
  showIcon?: boolean
}>()

(3) withDefaults はデフォルト値を提供します

VUE
<script setup lang="ts">
interface Props {
  user: User
  size?: 'small' | 'medium' | 'large'
  showEmail?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  showEmail: true
})
</script>

(4) 実行時宣言と型宣言の比較

次元 実行時間 defineProps({}) タイプ defineProps<T>()
型推論 弱 強(IDEの自動補完)
デフォルト値 直接のデフォルト値 withDefaults ラッパー
検証 バリデータ関数 TypeScriptのコンパイル時チェック
おすすめ 小規模プロジェクト/純粋なJS 大規模プロジェクト/TSプロジェクト

5. emit の完全な構文

(1) イベント文の5つの種類

VUE
<script setup>
// 1. A Brief Statement
const emit = defineEmits(['click', 'submit', 'cancel'])

// 2. Parameter Validation
const emit = defineEmits({
  addToCart: (productId) => typeof productId === 'number',
  remove: (id) => typeof id === 'string'
})

// 3. TypeScript Type
const emit = defineEmits<{
  'add-to-cart': [productId: number]
  'remove-item': [id: string, reason: string]
  'update': [id: number, data: object]
}>()
</script>

(2) 5つのトリガー手法

VUE
<script setup>
const emit = defineEmits(['click', 'submit', 'add-to-cart'])

// 1. Simple Trigger
emit('click')

// 2. Passing Parameters
emit('add-to-cart', 123)

// 3. Pass multiple parameters
emit('submit', { name: 'Alice' }, 2026)

// 4. Conditional Trigger
if (isValid) emit('submit')

// 5. Functional Triggering
function handleAdd() {
  emit('add-to-cart', props.product.id)
}
</script>

(3) 親コンポーネントのリスニング

(4) emit の型検証

VUE
<script setup>
const emit = defineEmits({
  // ✅ Verification:id It must be number
  remove: (id) => typeof id === 'number' || 'Validation failed',
  
  // ❌ Throws Error: When parameters do not match
  // remove: 'invalid',  // Wrong: It must be a function
})
</script>

▶ サンプル:ProductCard を使用する親コンポーネント

VUE
<!-- src/views/ProductListView.vue -->
<template>
  <div class="grid">
    <ProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      :show-stock-badge="true"
      @add-to-cart="handleAddToCart"
      @quick-view="handleQuickView"
      @toggle-favorite="handleToggleFavorite"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useCart } from '@/composables/useCart'

const products = ref([
  { id: 1, name: 'iPhone', price: 999, stock: 50, image: 'iphone.jpg' },
  { id: 2, name: 'MacBook', price: 2499, stock: 0, image: 'macbook.jpg' }
])

const { addToCart } = useCart()

function handleAddToCart(productId) {
  addToCart(productId)
}
function handleQuickView(productId) {
  console.log('Quick view:', productId)
}
function handleToggleFavorite(productId, isFavorite) {
  console.log('Toggle:', productId, isFavorite)
}
</script>
▶ 試してみよう

出力:

TEXT
iPhone(stock 50)は Add to Cart 有効、MacBook(stock 0)は Out of Stock で無効
ボタンを押すと add-to-cart が発火し handleAddToCart(1) が呼ばれる
quick-view と toggle-favorite もそれぞれコンソールに出力される

▶ サンプル:v-model による双方向バインディング

VUE
<!-- Child component CustomInput.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- Using Parent Components v-model -->
<template>
  <CustomInput v-model="searchQuery" />
  <p>Query: {{ searchQuery }}</p>
</template>

<script setup>
import { ref } from 'vue'
import CustomInput from '@/components/CustomInput.vue'
const searchQuery = ref('')
</script>
▶ 試してみよう

出力:

TEXT
初期表示:Query:(空)
入力欄に vue と打つと Query: vue が即時反映
子は update:modelValue を emit するだけで searchQuery を直接変更しない

6. データの一方向フローの原則

(1) 基本原則

100%
graph LR
    A[Parent Component data] -->|props| B[Child component]
    B -->|emit| A
    
    style A fill:#42b883
    style B fill:#42b883

(2) 5つの例外

VUE
<script setup>
const props = defineProps({ user: Object })

// ❌ Wrong: Edit prop directly
props.user.name = 'Bob'  // Vue Warning!

// ✅ Exception 1:prop When it is a reference type,Can be replaced with a new object
// The parent component passes a reference,Child components cannot change their references,But it's possible emit Let Father Change It

// ✅ Exception 2:prop Initial values can be saved locally
const localUser = ref({ ...props.user })
// Then make the changes localUser(Does not affect the parent)

// ✅ Exception 3: Use computed Derivative
const userName = computed(() => props.user.name)

// ✅ Exception 4: Use v-model (Syntax Sugar)
// <Child v-model="value" /> equivalent to :value + @update:value

// ✅ Exception 5:provide/inject Across levels
// Parent provide, Child inject (Learned in Phase 2.5)
</script>

(3) 5つのアンチパターン

JS
// ❌ Anti-pattern 1:Edit directly prop
props.user.name = 'Bob'

// ❌ Anti-pattern 2:Use in child components watch Edit prop
watch(() => props.value, (val) => { props.value = val * 2 })

// ❌ Anti-pattern 3: Two-way bound ref passthrough (Not recommended)
const localRef = ref(props.value)
watch(localRef, (val) => emit('update', val))

// ❌ Anti-pattern 4:Usage v-model Does not follow the naming convention
// v-model Expectations update:xxx Event,Custom event names must be consistent
emit('change', val)  // ❌ Parent Component @change Will not be triggered
emit('update:value', val)  // ✅ v-model Supporting

// ❌ Anti-pattern 5:Managing the parent component's data directly within a child component
emit('update', { ...props.user, name: 'Bob' })  // Wrong: Should only pass ID, Parent updates itselff

7. プロパティのパススルー:inheritAttrs と useAttrs

(1) デフォルトの動作

(2) 自動パススルーを無効にする

(3) useAttrsの明示的な使用

(4) 5つのユースケース

シナリオ 方法
デフォルト 何もする必要はありません。クラスはルートに自動的に適用されます
カスタムルート要素 useAttrs() 明示的なバインディング
ルート以外の要素へ展開 inheritAttrs: false + v-bind="$attrs"
複数のコンポーネント(フラグメント) inheritAttrs: false および明示的なバインディングを使用する必要がある
パフォーマンスの最適化 不要なDOM属性を回避するためにパススルーを無効にする

▶ サンプル:useAttrs のパススルー

▶ 試してみよう

出力:

TEXT
label の Name は label 要素に、placeholder と class="input-field" は input に付く
入力すると update:modelValue が emit され親の name が更新される

(1) 📊 . よくある5つの間違いに関するクイックリファレンス

エラー 症状 解決策
プロパティを直接変更 Vueの警告 emit を使用して親が変更できるようにする
配列/オブジェクト:propsには参照としてのデフォルト値が設定される 複数のコンポーネントで共有される default: () => [] ファクトリ
v-model のイベント名の不一致 親要素にイベントが伝わりません update:xxx を使用してください
バリデータは文字列を返す Vueはエラーを発生させない true/falseを返す
複数のコンポーネントへのパススルー Vueの警告 inheritAttrs: false

(2) 📊 . props の 5 つの主要な検証シナリオ

シナリオ 検証
メールアドレス val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
URL val => /^https?:\/\//.test(val)
列挙値 val => ['a', 'b', 'c'].includes(val)
範囲 val => val >= 0 && val <= 100
カスタムオブジェクト val => val.id && val.name


8. 総合演習:商品カードの props と emits

▶ サンプル:ProductCard.vue—props/emits の完全な記述

▶ 試してみよう

出力:

TEXT
stock 50 → $999 と Add to Cart(有効)
stock 5 → Only 5 left バッジ+ Add to Cart
stock 0 → Out of Stock+ボタン無効、カードは opacity 0.6


関連テンプレートとヘルパー:useAttrs() v-model

❓ よくある質問

Q プロップを直接変更することはできますか?
A いいえ。Vueの一方向データフローの原則に従い、プロップは読み取り専用です。データを変更するには、子コンポーネントがイベントを発行し、親コンポーネントがその変更を行う必要があります。プロップを直接変更すると、Vueから警告が表示されます。
Q 複雑なデータ(ネストされたオブジェクト)をどのように渡せばよいですか?
A 参照を直接渡してください。Vueが自動的にディープバインディングを処理します。オブジェクト内のプロパティを変更すると、親コンポーネントはその変更を検知します(ディープバインディング)。ただし、子コンポーネントはそれを直接変更してはいけません。代わりに、emit を使用して、親コンポーネントにオブジェクト全体またはプロパティを更新させる必要があります。
Q defineProps オプションと props オプションの違いは何ですか?
A <script setup> では defineProps() マクロを使用します(インポートは不要)。Options API では props: {} オプションを使用します。これら2つは同等ですが、setup + defineProps の使用をお勧めします。
Q emit は子コンポーネント内で非同期に呼び出すことはできますか?
A はい。emit は同期関数呼び出しですが、setTimeout や Promise: setTimeout(() => emit('done'), 1000) でラップすることができます。
Q v-modelの本質は何ですか?
A これはpropsおよびemitのための構文糖です。<Child v-model="x" />は:modelValue="x" @update:modelValue="x = $event"と同等です。詳しい説明については、このレッスンの例3を参照してください。
Q 子コンポーネントのプロパティの変更を監視するにはどうすればよいですか?
A watch(() => props.xxx, (newVal, oldVal) => {...}) を使用してください。監視処理内でプロパティを変更しないように注意してください。そうしないと、無限ループに陥ってしまいます。
Q useAttrs はどのような場合に使用すべきですか?
A 以下の 3 つのケースです:(1) マルチルートコンポーネント(フラグメント)で必要とされる場合;(2) 非ルート要素に属性を渡したい場合;(3) 属性がバインドされる場所を明示的に制御したい場合。通常のシングルルートコンポーネントでは必要ありません。
Q プロパティのバリデーションに失敗した場合はどうなりますか?
A 開発環境では、Vue は警告を表示します。本番環境では、エラーは黙って処理されます。バリデータは false(エラー)または文字列(エラーメッセージ)を返すことを推奨します。

📖 まとめ


📝 練習問題

  1. 基礎問題(難易度:⭐)

    簡単なButtonコンポーネントを実装します:

    • props: text (文字列), variant (文字列, 'primary'/'success'/'danger'), disabled (ブール値)
    • 3つのバリエーションは、3つのCSSクラスに対応しています
    • click イベントを発行する(パラメータなし)
    • 親コンポーネント内の3つのバリエーションをテストする
  2. 上級問題(難易度:⭐⭐)

    双方向バインドされた Input コンポーネントを実装する:

    • 子コンポーネント CustomInput.vue: props: modelValue, label
    • v-model という構文糖衣の使用
    • emit update:modelValue
    • 親コンポーネント:ユーザー入力をリアルタイムで表示します
  3. チャレンジ問題(難易度:⭐⭐⭐)

    包括的な「商品カード+お気に入り」システムを導入する:

    1. ProductCard.vue:すべてのプロパティ(product/showStockBadge/maxStock)+エミット(add-to-cart/toggle-favorite)
    2. App.vue:商品一覧 + お気に入りステータス
    3. お気に入りボタン:toggle-favorite を発行すると、親コンポーネントがお気に入りのステータスを更新します
    4. useAttrs および inheritAttrs を使用して、マルチルートコンポーネントを実装する(演習の内容を損なわない範囲で)
    5. 少なくとも3人のバリデーターによる検証(メールアドレス/URL/列挙型)
    6. TypeScriptの厳密型付けされたプロップ
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%