Vue.jsにおける子から親へのイベント発行($emit)

2025-07-29

カスタムイベントの基本概念とデータフロー

Vue.jsにおいて、$emitは子コンポーネントから親コンポーネントへ通信するための重要なメカニズムです。前章で学んだpropsが親→子の一方通行のデータフローであったのに対し、$emitはその逆方向の通信を可能にします。

カスタムイベントのデータフロー図

[親コンポーネント]
   ↑ (イベントを受け取る)
[子コンポーネント]
   └── イベントを発行($emit)

この双方向のコミュニケーションパターンは、Vueのコンポーネント間通信の基礎となります。

$emitの基本的な使い方

1. 子コンポーネントでのイベント発行

子コンポーネントで$emitメソッドを使用してイベントを発行します。

<template>
  <button @click="notifyParent">親に通知</button>
</template>

<script>
export default {
  methods: {
    notifyParent() {
      // 'custom-event'というイベントを発行
      this.$emit('custom-event', '子からのデータ')
    }
  }
}
</script>

2. 親コンポーネントでのイベント受信

親コンポーネントではv-on(または@)でイベントをリッスンします。

<template>
  <div>
    <!-- 子コンポーネントのイベントをリッスン -->
    <ChildComponent @custom-event="handleCustomEvent" />
    <p>親が受け取ったデータ: {{ receivedData }}</p>
  </div>
</template>

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

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      receivedData: ''
    }
  },
  methods: {
    handleCustomEvent(data) {
      this.receivedData = data
    }
  }
}
</script>

イベント名の規則とベストプラクティス

イベント名の命名規則

  • kebab-caseを使用(HTMLの属性名と一致させるため)
  • 意味のある名前を選択(動作を明確に表現)
  • プレフィックスを検討(大規模アプリケーション向け)
// 良い例
this.$emit('update-item', itemData)
this.$emit('form-submitted', formData)

// 悪い例
this.$emit('updateItem', itemData) // camelCaseは避ける
this.$emit('click') // ネイティブイベントと衝突する可能性

emitsオプションでの宣言(Vue 3推奨)

Vue 3では、コンポーネントが発行するイベントを明示的に宣言できます。

export default {
  emits: ['custom-event', 'submit-form'],
  methods: {
    triggerEvent() {
      this.$emit('custom-event', data)
    }
  }
}

実践的な使用例

フォーム入力コンポーネント

<!-- 子コンポーネント (InputField.vue) -->
<template>
  <div>
    <label>{{ label }}</label>
    <input 
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </div>
</template>

<script>
export default {
  props: ['label', 'modelValue'],
  emits: ['update:modelValue']
}
</script>

<!-- 親コンポーネント -->
<template>
  <div>
    <InputField 
      label="ユーザー名"
      :modelValue="username"
      @update:modelValue="username = $event"
    />
    <InputField 
      label="メールアドレス"
      :modelValue="email"
      @update:modelValue="email = $event"
    />
  </div>
</template>

<script>
import InputField from './InputField.vue'

export default {
  components: { InputField },
  data() {
    return {
      username: '',
      email: ''
    }
  }
}
</script>

高度なイベントパターン

複数の引数を渡す

// 子コンポーネント
methods: {
  sendMultiple() {
    this.$emit('multi-data', arg1, arg2, arg3)
  }
}

// 親コンポーネント
<ChildComponent @multi-data="handleMultiData" />

methods: {
  handleMultiData(arg1, arg2, arg3) {
    // 複数の引数を処理
  }
}

オブジェクトを渡す

// 子コンポーネント
methods: {
  sendObject() {
    this.$emit('object-event', {
      id: 123,
      name: 'テストアイテム',
      status: 'active'
    })
  }
}

// 親コンポーネント
methods: {
  handleObject(data) {
    console.log(data.id, data.name, data.status)
  }
}

イベントバスパターン(小規模アプリ向け)

// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 子コンポーネント (イベント発行側)
import { EventBus } from './eventBus'
methods: {
  emitGlobal() {
    EventBus.$emit('global-event', data)
  }
}

// 親コンポーネント (イベント受信側)
import { EventBus } from './eventBus'
created() {
  EventBus.$on('global-event', data => {
    console.log('グローバルイベントを受信:', data)
  })
}

カスタムイベントの応用例

モーダルダイアログコンポーネント

// 子コンポーネント (ModalDialog.vue)
<template>
  <div class="modal" v-if="visible">
    <div class="modal-content">
      <slot></slot>
      <button @click="close">閉じる</button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: Boolean
  },
  emits: ['close'],
  methods: {
    close() {
      this.$emit('close')
    }
  }
}
</script>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}
.modal-content {
  background: white;
  padding: 20px;
  border-radius: 5px;
}
</style>

// 親コンポーネント
<template>
  <div>
    <button @click="showModal = true">モーダルを開く</button>
    <ModalDialog 
      :visible="showModal" 
      @close="showModal = false"
    >
      <h2>モーダルタイトル</h2>
      <p>モーダルの内容がここに入ります</p>
    </ModalDialog>
  </div>
</template>

<script>
import ModalDialog from './ModalDialog.vue'

export default {
  components: { ModalDialog },
  data() {
    return {
      showModal: false
    }
  }
}
</script>

よくある間違いとベストプラクティス

1. イベント名の一貫性を保つ

// 悪い例 - イベント名がバラバラ
this.$emit('update')
this.$emit('changed')
this.$emit('modified')

// 良い例 - 一貫した命名規則
this.$emit('update-item')
this.$emit('item-updated')

2. 適切なイベントスコープを選択

// ローカルイベント(親子間)には$emitを使用
this.$emit('local-event')

// グローバルな通信にはVuexやPiniaを使用
this.$store.commit('updateState', data)

3. イベントハンドラーの分離

// 悪い例 - インラインで複雑な処理


// 良い例 - メソッドに分離


methods: {
  handleEvent(value) {
    this.value = value
    this.process(value)
    this.update()
  }
}

パフォーマンスに関する考慮事項

  1. 不要なイベント発行を避ける
    本当に必要な時だけイベントを発行
  2. イベントリスナーのクリーンアップ
    コンポーネント破棄前にイベントリスナーを解除
// EventBus使用時
beforeDestroy() {
  EventBus.$off('event-name')
}
  1. デバウンス処理の実装
    頻繁に発行されるイベントに対して
import { debounce } from 'lodash'

methods: {
  handleInput: debounce(function(value) {
    this.$emit('search', value)
  }, 300)
}

実践的なユースケース

商品フィルターコンポーネント

// 子コンポーネント (ProductFilter.vue)
<template>
  <div class="filters">
    <select v-model="selectedCategory" @change="updateFilter">
      <option value="all">すべて</option>
      <option v-for="category in categories" :key="category">
        {{ category }}
      </option>
    </select>
    <input 
      v-model="priceRange" 
      type="range" 
      min="0" 
      max="100000"
      @input="updateFilter"
    >
  </div>
</template>

<script>
export default {
  props: ['categories'],
  data() {
    return {
      selectedCategory: 'all',
      priceRange: 0
    }
  },
  emits: ['filter-change'],
  methods: {
    updateFilter() {
      this.$emit('filter-change', {
        category: this.selectedCategory,
        maxPrice: this.priceRange
      })
    }
  }
}
</script>

// 親コンポーネント
<template>
  <div>
    <ProductFilter 
      :categories="categories" 
      @filter-change="applyFilter"
    />
    <ProductList :products="filteredProducts" />
  </div>
</template>

<script>
import ProductFilter from './ProductFilter.vue'
import ProductList from './ProductList.vue'

export default {
  components: { ProductFilter, ProductList },
  data() {
    return {
      categories: ['電子機器', '家具', '衣類'],
      products: [...], // 商品データ
      currentFilter: {
        category: 'all',
        maxPrice: 100000
      }
    }
  },
  computed: {
    filteredProducts() {
      return this.products.filter(product => {
        const categoryMatch = 
          this.currentFilter.category === 'all' || 
          product.category === this.currentFilter.category
        const priceMatch = 
          product.price <= this.currentFilter.maxPrice
        return categoryMatch && priceMatch
      })
    }
  },
  methods: {
    applyFilter(filter) {
      this.currentFilter = filter
    }
  }
}
</script>

まとめ

  • $emitは子から親への通信手段
  • イベント名はkebab-caseで一貫性を保つ
  • Vue 3ではemitsオプションで明示的に宣言
  • 複雑なデータもイベントで親に伝達可能
  • 適切なスコープでイベントを使用(ローカル vs グローバル)
  • パフォーマンスを考慮したイベント設計が重要

propsと$emitを組み合わせることで、Vue.jsのコンポーネント間通信を効果的に実装できます。この知識を活用して、より柔軟で保守性の高いアプリケーションを構築してください。