はじめに
defineEmitは子コンポーネントから親コンポーネントへイベントを発行(=宣言)する。言っていることはわかるにはわかるけど、わからない。Vue初心者の私はそう思っていました。
defineEmitとは何なのか?なぜ使う必要があるのか?これらを理解した上で実際の使い方を見ていきましょう!
defineEmitが何者なのかを理解してみよう
defineEmitとはなに?
ざっくり言うと、子コンポーネントから親コンポーネントへイベントを発行(=宣言)する。
流石にこれだけでは初心者には優しくないです。イベントを発行(=宣言)するってなに?ってなりますよね
イベントを発行(=宣言)するとは?
そもそもイベントとは
DOM 要素(例:)でブラウザによって発生した動作(クリック、入力など)を、Vue コンポーネント内で扱えるようにラップしたもの。
つまり「イベントを発行する」とは、
子コンポーネント内で発生したイベントを親へ知らせるということです。
私はなんで親に知らせる必要があるのか、疑問に思うのでもっと深掘っていきましょう。
何で親に知らせる必要があるの?
以下の理由から、子コンポーネントで発生したイベントを親に知らせる必要があるのです。
- UI操作がアプリケーション全体に影響を与える場合があるため
- 子コンポーネント内で発生したイベントを親コンポーネントで使いたい場合があるため
- 子コンポーネントは状態を持たない「ステートレス」であるべきという考え方がある
UI操作がアプリケーション全体に影響を与える場合があるため
フォームの送信ボタンをクリックしたとき、親コンポーネントやアプリ全体で処理したいケースがあります。
- 子コンポーネント:「送信ボタンがクリックされました」と通知する
- 親コンポーネント:それに応じて API 呼び出しやページ遷移を行う
<!-- 子コンポーネント -->
<template>
<button @click="submit">送信</button>
</template>
<script setup>
const emit = defineEmits(['submit'])
function submit() {
emit('submit') // ← 親に送信イベントを伝える
}
</script>
<!-- 親コンポーネント -->
<template>
<ChildComponent @submit="handleSubmit" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
function handleSubmit() {
// 実際の処理(API呼び出しや状態更新など)
}
</script>
子コンポーネント内で発生したイベントを親コンポーネントで使いたい場合があるため
例えば入力欄が子コンポーネントにある場合、その値を親で使いたいことがある。
<!-- 子コンポーネント -->
<template>
<input v-model="text" />
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const emit = defineEmits(['update:text'])
watch(text, (newVal) => {
emit('update:text', newVal)
})
</script>
<!-- 親コンポーネント -->
<template>
<ChildComponent :text="message" @update:text="message = $event" />
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
子コンポーネントは状態を持たない「ステートレス」であるべきという考え方がある
そもそも子コンポーネントは状態を持たないステートレスであるべきという考え方があります。UI コンポーネント(例:ボタン、ダイアログ、カレンダー)は、見た目だけを担当し、アプリケーションロジックには関与しないように分離されることが多いです。
そのため、子コンポーネント内で何らかの操作が起きたことを通知する手段としてイベントを使う必要があります。
💡結論
子コンポーネントから親コンポーネントにイベントを送る理由とは、「UI 上の出来事をアプリケーション全体で扱えるようにするため」です
defineEmitのメリット
親にイベントを知らせる理由がわかったところで、イベントの宣言として「defineEmit」を使って得られるメリットもさらっておきます。イベントを明示的に宣言することで下記のメリットを得ることができます。
- 可読性・保守性・型安全性が向上する
- 開発者体験が向上する
- IDE やツールのサポート
- 親コンポーネントとの契約を明確化
可読性・保守性・型安全性が向上する
どのイベントを送出するのかコード上で一目でわかります。TypeScriptを使っている場合はイベントの引数まで型を明確に宣言できます。
開発者体験が向上する
可読性・保守性・型安全性が向上することで、他の開発者も使いやすく、エラーを事前に検出できることにもつながります。
IDE やツールのサポート
明示的にイベントを定義しておくことで、以下の恩恵を受けられます。
- コード補完(例:emit. を入力したときに出る候補)
- イベント使用時の型チェック(例:引数のミスマッチを警告)
- Vue Devtools などでイベントが正しく認識されやすくなる
親コンポーネントとの契約を明確化
defineEmits
を使えば、「自分がどんなイベントを出すのか」を明確にし、親コンポーネントがそれに応じて対応できるようになります。
これは、コンポーネントの再利用性・保守性を高めるために有効です。
Vue の一方向データフローに基づく設計では、子コンポーネントが勝手に状態を変更することは避け、親コンポーネントが状態を管理し、処理を制御する のがベストプラクティスです。
基本的な使い方
イベント名を文字列の配列で指定
最もシンプルな例です。これで、click
とupdate
という2つのイベントを発行できるようになります。
<script setup>
const emit = defineEmits(['click', 'update'])
</script>
イベントを実際につかう
emit('click') // click イベントを発行
emit('update', '新しい値') // update イベントにデータも一緒に送る
TypeScript を使った型定義
TypeScript を使っている場合、イベントの引数の型まで指定できます。
const emit = defineEmits({
click: null, // 引数なし
update: (value: string) => typeof value === 'string' // 引数あり
})
実践例:入力フォームの更新イベント
子コンポーネント
<script setup lang="ts">
interface Emits {
(e: 'update', value: string): void
}
const emit = defineEmits<Emits>()
function handleChange(event: Event) {
const target = event.target as HTMLInputElement
emit('update', target.value)
}
</script>
<template>
<input type="text" @input="handleChange" />
</template>
@input="handleChange"
:
inputイベントにhandleChangeを登録しています
emit('update', target.value)
:
defineEmitsで宣言されたupdate
イベントを第一引数に指定し、第二引数には送信するデータを指定しています。
親コンポーネント
<script setup lang="ts">
import { ref } from 'vue'
import InputComponent from './InputComponent.vue'
const inputValue = ref('')
function handleUpdate(value: string) {
inputValue.value = value
}
</script>
<template>
<InputComponent @update="handleUpdate" />
<p>現在の値:{{ inputValue }}</p>
</template>
@update="handleUpdate"
:
updateイベントにhandleUpdate関数を登録しています。
これは「子から送られてきた値を、ロジックを挟んでから使いたい」場合に便利です。
ロジックを挟んでから使いたい ⇄ 関数を使わず直接バインドしたい
関数を使わず直接バインドしたい
<script setup lang="ts">
import { ref } from 'vue'
import InputComponent from './InputComponent.vue'
const inputValue = ref('')
</script>
<template>
<!-- $event で受け取った値を直接代入 -->
<InputComponent @update="inputValue = $event" />
<p>入力値:{{ inputValue }}</p>
</template>
$event は Vue が自動で用意してくれる変数で、イベントで送られてきた引数 を表します。
少し応用
実は、このケースはv-model
を使えばさらに簡潔になります。
子コンポーネント(modelValue / update:modelValue 使用)
<script setup lang="ts">
defineProps(['modelValue'])
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
function handleChange(event: Event) {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleChange" />
</template>
:value="modelValue"
:
input要素のvalue属性に親要素から受け取った現在の値を渡しています。(双方向バインディング想定)
@input="handleChange"
:
inputイベントにhandleChange関数を渡しています。
親コンポーネント側
<script setup lang="ts">
import { ref } from 'vue'
const inputValue = ref('')
</script>
<template>
<!-- v-model で双方向バインディング風に扱える -->
<InputComponent v-model="inputValue" />
<p>入力値:{{ inputValue }}</p>
</template>
v-model="inputValue"
:
:modelValue='inputValue'
+ :update:modelValue='inputValue=$event'
@update:modelValue は Vue が v-model の仕組みのために『デフォルトで使う』イベント名であり、「規約として決められた名前」ではあるものの、「固定された予約語」ではなく、カスタマイズ可能です。
まとめ
defineEmitとは何なのか?なぜ使う必要があるのか?実際の使い方は?をそれぞれ噛み砕いて説明していきましたが、いかがでしたでしょうか?v-modelに続いて、defineEmitも初心者を混乱させる構文の一つだと思います。現場でコードに触れていて、こんがらがることがあるので私自身の頭の中を整理する目的で記事にしてみました。
Vue初心者の方には共感、参考にしていただけたら嬉しいです!