採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

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-utilsemitted() で、emit されたイベントと値を検証できる:

await wrapper.get('[data-testid="inc"]').trigger('click')
expect(wrapper.emitted('change')[0][0]).toBe(6)   // 最初の change の第1引数

子が「正しい値を emit したか」だけを見ればよく、親を用意せずテストできる。