Vue.jsにおける親子コンポーネントのデータフロー徹底解説

2025-07-29

親子コンポーネントの基本概念

Vue.jsでは、コンポーネントを組み合わせることでアプリケーションを構築します。親子関係を持つコンポーネント間でデータをやり取りする方法は、Vueの重要なコンセプトです。

単一ファイルコンポーネント(SFC)との比較

単一ファイルコンポーネントが「1つのコンポーネントを1つのファイルに閉じ込める」ことを目的とするのに対し、親子コンポーネントの関係は「コンポーネント間の連携方法」に焦点を当てます。

<!-- ParentComponent.vue -->
<template>
  <div>
    <ChildComponent :message="parentMessage" @child-event="handleChildEvent" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: '親からのメッセージ'
    }
  },
  methods: {
    handleChildEvent(data) {
      console.log('子から受け取ったデータ:', data);
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>親からのメッセージ: {{ message }}</p>
    <button @click="sendToParent">親に送信</button>
  </div>
</template>

<script>
export default {
  props: {
    message: String
  },
  methods: {
    sendToParent() {
      this.$emit('child-event', '子からのデータ');
    }
  }
}
</script>

親から子へのデータフロー (Props Down)

基本のプロパティ渡し

<!-- ParentComponent.vue -->
<template>
  <ChildComponent :title="pageTitle" :content="pageContent" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      pageTitle: '親コンポーネントのタイトル',
      pageContent: 'これは親から渡されるコンテンツです'
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    },
    content: {
      type: String,
      default: 'デフォルトコンテンツ'
    }
  }
}
</script>

プロパティバリデーション

// ChildComponent.vue
export default {
  props: {
    // 基本の型チェック
    propA: Number,

    // 複数の型を許可
    propB: [String, Number],

    // 必須項目かつ文字列
    propC: {
      type: String,
      required: true
    },

    // デフォルト値
    propD: {
      type: Number,
      default: 100
    },

    // カスタムバリデータ関数
    propE: {
      validator(value) {
        return ['success', 'warning', 'danger'].includes(value);
      }
    },

    // オブジェクトのデフォルト値
    propF: {
      type: Object,
      default() {
        return { message: 'hello' };
      }
    }
  }
}

子から親へのデータフロー (Events Up)

カスタムイベントの発行

<!-- ChildComponent.vue -->
<template>
  <button @click="notifyParent">親に通知</button>
</template>

<script>
export default {
  methods: {
    notifyParent() {
      this.$emit('custom-event', { data: '子からのデータ' });
    }
  }
}
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent @custom-event="handleCustomEvent" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleCustomEvent(payload) {
      console.log('子から受け取ったデータ:', payload.data);
    }
  }
}
</script>

v-modelによる双方向バインディング

<!-- ParentComponent.vue -->
<template>
  <CustomInput v-model="inputValue" />
  <p>親コンポーネントの値: {{ inputValue }}</p>
</template>

<script>
import CustomInput from './CustomInput.vue';

export default {
  components: {
    CustomInput
  },
  data() {
    return {
      inputValue: ''
    }
  }
}
</script>
<!-- CustomInput.vue -->
<template>
  <input
    type="text"
    :value="value"
    @input="$emit('input', $event.target.value)"
  />
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      required: true
    }
  }
}
</script>

スロットを使ったコンテンツ配信

基本スロット

<!-- ParentComponent.vue -->
<template>
  <ChildComponent>
    <p>これはスロットに挿入されるコンテンツです</p>
  </ChildComponent>
</template>
<!-- ChildComponent.vue -->
<template>
  <div class="container">
    <h2>子コンポーネントのタイトル</h2>
    <slot>デフォルトコンテンツ(親から何も渡されない場合に表示)</slot>
  </div>
</template>

名前付きスロット

<!-- ParentComponent.vue -->
<template>
  <LayoutComponent>
    <template v-slot:header>
      <h1>カスタムヘッダー</h1>
    </template>

    <template v-slot:default>
      <p>メインコンテンツ</p>
    </template>

    <template v-slot:footer>
      <p>カスタムフッター</p>
    </template>
  </LayoutComponent>
</template>
<!-- LayoutComponent.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

スコープ付きスロット

<!-- ParentComponent.vue -->
<template>
  <DataList :items="items">
    <template v-slot:item="slotProps">
      <span>{{ slotProps.item.name }}</span>
      <span>{{ slotProps.item.price }}円</span>
    </template>
  </DataList>
</template>

<script>
import DataList from './DataList.vue';

export default {
  components: {
    DataList
  },
  data() {
    return {
      items: [
        { name: '商品A', price: 1000 },
        { name: '商品B', price: 2000 }
      ]
    }
  }
}
</script>
<!-- DataList.vue -->
<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      <slot name="item" :item="item"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  }
}
</script>

親子コンポーネントの高度な連携

provide/injectによる依存性注入

// 祖先コンポーネント
export default {
  provide() {
    return {
      themeData: {
        primaryColor: '#42b983',
        secondaryColor: '#35495e'
      }
    }
  }
}

// 子孫コンポーネント
export default {
  inject: ['themeData'],
  created() {
    console.log(this.themeData.primaryColor); // #42b983
  }
}

$refsによる直接アクセス

<!-- ParentComponent.vue -->
<template>
  <ChildComponent ref="child" />
  <button @click="callChildMethod">子のメソッドを呼び出す</button>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    callChildMethod() {
      this.$refs.child.childMethod();
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<script>
export default {
  methods: {
    childMethod() {
      console.log('子コンポーネントのメソッドが呼ばれました');
    }
  }
}
</script>

単一コンポーネント vs 親子コンポーネントの比較

特徴単一コンポーネント親子コンポーネント
目的1つの機能を自己完結コンポーネント間の連携
再利用性限定的高い
複雑さ単純関係性の管理が必要
データフロー内部のみProps/Eventsによる双方向通信
適したケース小規模なUI部品大規模アプリケーションの構築

ベストプラクティス

  1. Propsは一方通行:子コンポーネントでpropsを直接変更しない
  2. イベント名はケバブケース@my-eventのように命名
  3. 複雑なデータフローにはVuex/Pinia:深い階層のコンポーネント間通信に
  4. スロットで柔軟性向上:コンポーネントの再利用性を高める
  5. 過度な親子依存を避ける:疎結合な設計を心がける
<!-- Good Practice Example -->
<template>
  <SearchResults :items="filteredItems">
    <template #item="{ item }">
      <ProductCard :product="item" @select="addToCart" />
    </template>
  </SearchResults>
</template>

<script>
import SearchResults from './SearchResults.vue';
import ProductCard from './ProductCard.vue';

export default {
  components: {
    SearchResults,
    ProductCard
  },
  data() {
    return {
      allItems: [],
      searchQuery: ''
    }
  },
  computed: {
    filteredItems() {
      return this.allItems.filter(item => 
        item.name.includes(this.searchQuery)
      );
    }
  },
  methods: {
    addToCart(product) {
      // カートに追加するロジック
    }
  }
}
</script>

まとめ

Vue.jsの親子コンポーネント間のデータフローは、以下の3つの主要なパターンで構成されます:

  1. Props Down:親から子へのデータ伝達
  2. Events Up:子から親へのイベント通知
  3. Slots:コンテンツの柔軟な挿入

単一ファイルコンポーネント(SFC)がコンポーネントの「内部構造」を定義するのに対し、親子コンポーネントの関係は「コンポーネント間の連携方法」を定義します。適切に設計された親子関係は、以下の利点をもたらします:

  • 再利用性の向上:コンポーネントを様々なコンテキストで使用可能
  • 責務の分離:各コンポーネントが単一責任を負う
  • メンテナンス性の向上:変更が局所化される
  • 拡張性:新しい機能を既存の構造に組み込みやすい

大規模なVue.jsアプリケーションを構築する際には、これらのデータフローパターンを適切に組み合わせることで、整理された保守性の高いコードベースを維持できます。