【Vue】emitで子から親にイベントを伝える

JavaScript

こんにちは、かつコーチです。

前回は、親から子へデータを渡す仕組みであるPropsを解説しました。

今回は、その逆方向、子コンポーネントから親コンポーネントへ「何かが起きたこと」を伝える emit(イミット)を扱います。

「ボタンが押された」「フォームが送信された」といった子コンポーネント内の出来事を、親側に知らせたい場面は非常に多くあります。

Propsとemitはセットで理解して初めて、コンポーネント間のデータのやり取りが一通りできるようになります。

emitとは?

子から親への一方通行

前回、Propsは「親から子への一方通行」と説明しました。

emit は、その逆で「子から親への一方通行」の仕組みです。

子コンポーネントは、「こういう出来事(イベント)が起きました」ということだけを親に知らせます。

具体的にどう処理するかは、あくまで親コンポーネント側が決める、という役割分担になっています。

なぜ子から親に直接データを書き換えないのか

「子コンポーネントから直接親のデータを書き換えられたら楽なのでは」と思うかもしれません。

しかし、それを許してしまうと、どこでデータが書き換わったのか追跡しづらくなり、バグの温床になります。

Vueが「Propsは親から子への一方通行」「データを変えたいときはemitでイベントを発行し、親側で処理する」という設計にしているのは、データの流れを一方向に保ち、追跡しやすくするためです。

この考え方は「単方向データフロー」と呼ばれ、Vueに限らず多くのフロントエンドフレームワークで採用されている基本原則です。

emitの基本の書き方

子コンポーネント側でイベントを発行する

子コンポーネント(LikeButton.vue を想定)で defineEmits を使います。

<script setup lang="ts">
interface Emits {
  (e: 'like'): void
}

const emit = defineEmits<Emits>()

const handleClick = (): void => {
  emit('like')
}
</script>

<template>
  <button @click="handleClick">いいね!</button>
</template>

defineEmits<Emits>() で「このコンポーネントはどんなイベントを発行するか」を型付きで宣言します。

ボタンがクリックされたら emit('like') を実行し、「likeイベントが起きました」ということだけを親に伝えます。

親コンポーネント側でイベントを受け取る

親コンポーネント側では、@v-on の省略形)を使ってイベントを受け取ります。

<script setup lang="ts">
import { ref } from 'vue'
import LikeButton from './LikeButton.vue'

const likeCount = ref<number>(0)

const increaseLike = (): void => {
  likeCount.value++
}
</script>

<template>
  <p>いいね数: {{ likeCount }}</p>
  <LikeButton @like="increaseLike" />
</template>

子コンポーネントで emit('like') が実行されると、親側で登録した increaseLike 関数が呼び出されます。

「子はボタンが押されたことだけを伝える」「実際にカウントを増やす処理は親が持つ」という役割分担がはっきりしているのがポイントです。

emitで値を一緒に渡す

引数付きでイベントを発行する

emit は、イベント名と一緒に値を渡すこともできます。

<script setup lang="ts">
interface Emits {
  (e: 'update-name', newName: string): void
}

const emit = defineEmits<Emits>()

const handleInput = (event: Event): void => {
  const target = event.target as HTMLInputElement
  emit('update-name', target.value)
}
</script>

<template>
  <input type="text" @input="handleInput" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import NameInput from './NameInput.vue'

const userName = ref<string>('')

const handleUpdateName = (newName: string): void => {
  userName.value = newName
}
</script>

<template>
  <p>入力された名前: {{ userName }}</p>
  <NameInput @update-name="handleUpdateName" />
</template>

子コンポーネントは入力された値を emit('update-name', target.value) として親に渡し、親側の handleUpdateName がその値を受け取って自分のデータを更新します。

このように、「値の変更自体は親が行い、子は変更のきっかけとなる情報だけを伝える」という流れを徹底することで、データの流れが追いやすくなります。

つまずきやすいポイント:イベント名の不一致

ケバブケースとキャメルケースの食い違い

私が実際にハマったのが、イベント名の表記ゆれによる「イベントが発火しない」というトラブルでした。

❌ Before:定義と受け取りでイベント名の表記がずれている

<script setup lang="ts">
interface Emits {
  (e: 'updateName', newName: string): void // キャメルケースで定義
}

const emit = defineEmits<Emits>()
</script>
<template>
  <!-- テンプレート内ではケバブケースで書いてしまい、反応しない -->
  <NameInput @update-name="handleUpdateName" />
</template>

スクリプト側で updateName というキャメルケースのイベント名を定義したのに、テンプレート側で @update-name というケバブケースで受け取ろうとして、なぜかイベントが発火しない、という状態にしばらく気づけませんでした。

実際にはVueが自動的にケース変換をしてくれる場面もあるのですが、思い込みで書いてしまうと混乱の元になります。

✅ After:イベント名の表記を統一する

<script setup lang="ts">
interface Emits {
  (e: 'update-name', newName: string): void // ケバブケースで統一
}

const emit = defineEmits<Emits>()

const handleInput = (event: Event): void => {
  const target = event.target as HTMLInputElement
  emit('update-name', target.value)
}
</script>
<template>
  <NameInput @update-name="handleUpdateName" />
</template>

defineEmits の定義とテンプレート内の @ 属性、両方でイベント名の表記を統一しておくことで、こうした食い違いを防げます。

迷ったら「イベント名はケバブケース(update-nameのようにハイフン区切り)で統一する」というルールを自分の中に持っておくと安全です。

まとめ

この記事のポイント

  • emit は子コンポーネントから親コンポーネントへイベントを伝える、一方通行の仕組み
  • defineEmits<Emits>() でイベント名と引数の型を定義してから emit を実行する
  • 実際のデータ更新は親側の処理に任せ、子は「起きたこと」を伝えるだけにする
  • イベント名の表記(キャメルケース/ケバブケース)は、定義側と受け取り側で統一する

次に読むべき記事

次回は、コンポーネントの中身を外から差し込める「スロット(slot)」を使って、再利用可能なコンポーネントを作る方法を解説します。

→ 次の記事:スロット(slot)で再利用可能なコンポーネントを作る

タイトルとURLをコピーしました