Vue — Emits (子→親)
テクノロジ系 / L02 component-design — コンポーネント設計
Vue — Emits (子→親)
なぜ emit
Vue は props 降り / emit 上り (React と同じ単方向)
子は親に イベントを発行 することで通知する。
直接親の props を書き換えない。
基本
<!-- Counter.vue (子) -->
<script setup lang="ts">
const emit = defineEmits<{
(e: "increment"): void;
(e: "set", value: number): void;
}>();
</script>
<template>
<button @click="emit('increment')">+</button>
<button @click="emit('set', 0)">Reset</button>
</template>defineEmits<...>() で型定義。
親側
<script setup>
import Counter from "./Counter.vue";
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<p>{{ count }}</p>
<Counter
@increment="count++"
@set="(v) => count = v"
/>
</template>@event-name="..." でハンドラ。kebab-case で書く慣習。
defineModel (Vue 3.4+)
v-model を子コンポーネントで:
<!-- MyInput.vue --> <script setup lang="ts"> const model = defineModel<string>(); </script> <template> <input v-model="model" /> </template>
<!-- 親 --> <MyInput v-model="text" />
→ defineProps + defineEmits + 同期処理を 1行に圧縮。
ラッパー input で重宝。
複数 v-model
<!-- 子 -->
<script setup lang="ts">
const firstName = defineModel<string>("first");
const lastName = defineModel<string>("last");
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
</template><!-- 親 --> <NameInput v-model:first="firstName" v-model:last="lastName" />
emit のバリデーション (旧スタイル)
<script setup>
const emit = defineEmits({
submit: (payload: { name: string; age: number }) => {
return payload.name !== "" && payload.age > 0;
},
});
</script>→ TS で型を書けば十分なので、新規では不要。
まとめ
defineEmits<...>()で型定義emit("event", args)で発火- 親は
@event="..."で受ける - Vue 3.4+ は
defineModelで v-model を簡潔に - 複数 v-model は引数で識別
次は slot。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — defineEmits
問題
<!-- Counter.vue -->
<script setup lang="ts">
const emit = defineEmits<{
increment: [];
set: [value: number];
}>();
const props = defineProps<{ count: number }>();
</script>
<template>
<p>{{ props.count }}</p>
<button @click="emit('increment')">+</button>
<button @click="emit('set', 100)">100</button>
</template>
<!-- 親 -->
<script setup>
const c = ref(0);
</script>
<template>
<Counter :count="c" @increment="c++" @set="(v) => c = v" />
</template>合格基準
- 子から親への通知が動く
- 引数あり emit も動く
Lv2: 応用 — defineModel (3.4+)
問題
<!-- DoubleInput.vue -->
<script setup lang="ts">
const text = defineModel<string>();
</script>
<template>
<input v-model="text" />
<input v-model="text" /> <!-- 同じ state にバインド -->
</template>
<!-- 親 -->
<script setup>
const t = ref("");
</script>
<template>
<DoubleInput v-model="t" />
<p>Parent: {{ t }}</p>
</template>2 input が同期し、親の state も連動。
合格基準
- defineModel で簡潔
- 両 input + 親が同期
Lv3: 発展 — 複数 v-model
問題
<!-- NameForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>("first");
const lastName = defineModel<string>("last");
</script>
<template>
<input v-model="firstName" placeholder="First" />
<input v-model="lastName" placeholder="Last" />
</template>
<!-- 親 -->
<NameForm v-model:first="fn" v-model:last="ln" />
<p>Full: {{ fn }} {{ ln }}</p>合格基準
- 2フィールドが個別に v-model
- 親で両方使える
提出
submission.md に3問の回答を。
ヒント
defineEmits(['change'])で「このコンポーネントが発火するイベント名」を宣言し、戻り値をemitに代入する。- イベントを起こすときは
emit('イベント名', 渡す値)。ここでは新しい数値を渡す。 - 子コンポーネントは 自分で値を書き換えない。
props.value + 1を計算して emit するだけ。実際に値を更新するのは親の役目(一方向データフロー)。
解説
emit は「子 → 親」の上り通信
props(ch01)は親 → 子の 下り。emit はその逆、子 → 親の 上り:
const props = defineProps({ value: { type: Number, required: true } }) // 下り
const emit = defineEmits(['change']) // 上り子は「何か起きた」を emit('change', 新しい値) で親に通知するだけ。
どう反応するか(実際に値を更新するか)は親が決める。
なぜ子が自分で値を持たないのか(一方向データフロー)
このステッパーは、現在値を props で受け取り、+/- が押されたら 新しい値を emit する だけ。
子は値を書き換えない。これが Vue の 一方向データフロー:
親が value を持つ → 子に props で渡す(下り)
子が +/- → emit('change', 新値)(上り)→ 親が value を更新 → また props で下るメリット:
- 「値の出どころ」が常に親 1 か所。どこで値が変わったか追える。
- 子は再利用しやすい(親が誰でも、値の管理方法を知らなくてよい)。
> もし双方向にしたいなら v-model を使う(update:modelValue を emit する規約)。
> その仕組みも内部はこの emit。まず「子は emit するだけ、更新は親」を体に入れる。
テストでの確かめ方
@vue/test-utils の emitted() で、emit されたイベントと値を検証できる:
await wrapper.get('[data-testid="inc"]').trigger('click')
expect(wrapper.emitted('change')[0][0]).toBe(6) // 最初の change の第1引数子が「正しい値を emit したか」だけを見ればよく、親を用意せずテストできる。