🎁

【Vue.js/TypeScript】 emitで理解するVue.jsの本質

2023/01/06に公開

この記事は?

著者はReactを軸に開発をおこなってきたが、仕事の関係でVueをやることになって「emit」周りの理解が難しく、調べてもなかなかわかりづらかったので改めて記事にすることにした。

対象読者

・Vueのemitを使う理由がわからない人
・Vue3•TSでのemitを使い方を知りたい人

Vueの基礎(復習)

この記事では以下のようなことがわかることは前提にしています。

ref/reactive
値が監視され、変更が検知される状態のことをリアクティブといい、refはプリミティブな値をリアクティブにし、reactiveはObjectの値をリアクティブした変数を作る。
props
Vueでは親から個コンポーネントにpropsを経由して情報を渡す。
v-model
Vueではデータの変更とViewが同期する双方向バインディングを提供しており、v-modelを使って変数と入力フォームで保持している値を結びつける。
computed
通常のメソッドと違い算出プロパティではリアクティブな依存関係にもとづきキャッシュされ、依存関係に差分がない場合余分に計算をせずにすぐに結果を返すことができる。後述するが、算出プロパティはデフォルトではgetter関数のみだが、必要があればsetter関数も使うことができる。

emitとはなにか?

Vue.jsでは、子コンポーネントで何らかの処理が行われた時に、emit機能を用いて子コンポーネントを使用している親コンポーネントに対してイベントを発生・変更を通知させている。

なぜemitが必要なのか?

①Vue.jsでは親から子にpropsを渡すだけで、つまり、propsはイミュータブル(=変更不可能)になっているため渡した子コンポーネントでの変更はできない。

→もし子コンポーネントで無理やり変更しようとすると以下のようなエラーになる。
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

②Vue.jsでは親から子コンポーネントに渡したpropsは監視対象にならず、親から渡された変数が渡した先で変わったとしても監視されておらず、子コンポーネントは再レンダリングされない。

→よって、propsを渡すだけでは親子全体のレンダリングで不都合が生じる。

①はVue.jsが、単一方向のデータ渡ししかできないようにする事で、データフローを親→子へのみに限定し、データフローの複雑化を防止してシンプルになるようする工夫、②は無駄なレンダリングを防ぐためのVue.jsの設計上の工夫だと考えることができる。(一方、React.jsではコンポーネントの状態 (propsやstate)が変更された場合に、変更されたコンポーネントとその子コンポーネントも含めて、再レンダリングが実行されるので、注意して余分なレンダリングを防ぐ必要がある。

話を戻すと、Vue.jsでは①の②の問題を解決するために使うのがemitである。

emitをどう使うのか?

emitは次の手順で使うことが可能です。

・親コンポーネントでpropsを渡す
v-model:valで渡せばupdate関数は含まれており明示的に渡さなくて良い。
・子でpropsを受け取り、computedで親コンポーネントから受け取った値をsetする。
・同時にcomputedで子で新たに更新された値をsetすることで親に変数差分を伝える。
・「computedされた値」をinput要素に渡すことで双方向バインディングを行なう。

親コンポーネント

<template>
  <ChildModal v-model:visible="isModalOpen" />
<!-- これは下記の省略形です -->
  <ChildModal
  :visible="isModalOpen"
  @update:visible="isModalOpen = $event"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const isModalOpen = ref<boolean>(false)
</script>

子コンポーネント

<template>
  <a-modal v-model:visible="visibleComputed" />
</template>

<script setup lang="ts">
import { computed } from 'vue'
interface Emits {
    (e: "update:visible", visible: boolean): void;
}

interface Props {
    visible: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const computedData = computed({
    get: () => props.visible,
    set: (value) => {
        emit("update:visible", value);
    },
});
</script>

まとめ

・Vueでは子から親へイベント発生を伝えるためにemitを使う
・Vueでは親から子へ渡したpropsを直接変更することはできない
・Vueでは子コンポーネントに変数を渡すだけでは変更によって子は再レンダリングされない
・Vueではこれらの問題を解決するためにemitを用いることができる

参考文献

https://qiita.com/yuta-katayama-23/items/5e0396df42a95ef96239
https://prograshi.com/language/vue-js/avoid-mutating-a-prop-directly/
https://qiita.com/doz13189/items/e58868d4ec037f572df2#vue-の再レンダリングのタイミングを確かめる
https://tech.andpad.co.jp/entry/2022/05/24/100000

Discussion