こんにちは、かつコーチです。
前回は、親から子へデータを渡す仕組みである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)で再利用可能なコンポーネントを作る