thumbnail

ProvideとInjectをComposition APIとTypeScriptで解説

Vueにおいてコンポーネントの階層が深くなったとき、浅い階層から深い階層へデータを渡すためにバケツリレー(props drilling)が発生することがあります。そのために、データを一元管理するVuexやPiniaなどの外部ライブラリを使うことが一般的です。しかしアプリの規模が小さい場合や外部ライブラリがオーバースペックになる場合は、Vueに備えられているProvideとInjectを利用する手もあります。この記事ではVue3におけるProvideとInjectをComposition APIとTypeScriptで解説していきます。

ProvideとInjectで出来ること

出来ることはとてもシンプルで、親で定義したデータを”バケツリレー(props drilling)しないで”子孫コンポーネントに直接渡せます。

VuexやPiniaなどの状態管理用の外部ライブラリほど複雑なことは出来ません。

provideとinjectの基本

Composition APIでの使い方

親コンポーネントにおいて、子孫に供給したいデータをprovide関数に渡します。
その際に、データを一意に識別するキーを第1引数に指定します。

<script setup lang="ts">
import { provide } from "vue"

provide(キー, 渡したいデータ)
</script>

供給されたデータを使いたい子孫コンポーネントは、inject関数でデータを受け取ります。
その際に親で指定したキーを引数に渡します。

<script setup lang="ts">
import { inject } from "vue"

const useData = inject(キー)
</script>

キーに対応するデータがない場合に備えて、injectの第2引数にはデフォルト値を渡すことも可能です。

inject(キー, デフォルト値)

TypeScriptでの記述方法

TypeScriptを用いた場合、キーの指定方法は2パターンあります。

  • ① 文字列キーを使用
  • ② InjectionKeyを使用

の2パターンです。どちらを使うかによって型指定やデータの受け取り方が異なるので、順番に解説していきます。

文字列キーを使用する方法

文字列キーを使用した場合のprovide

文字列キーとは、文字通り「string型のキー」を使用する方法です。
次のようにして使います。キーの名前は任意です。

//"key1"という文字列キーにnumber型の30を渡す
provide("key1", 30)

//"key2"という文字列キーにstring型の"hoge"を渡す
provide("key2", "hoge")

provideに渡すデータの型を明示的に指定していませんが、この時点でTypeScriptによって自動的に型推論が行われています。

推論される型よりも詳細に型を指定したい場合など、明示的に型を指定したい場合はジェネリックで渡します。

// string | number型のデータ
provide<string | number>("fuag", hoge)

// number[]型のデータ
provide<number[]>("fuga", fuga)

データを供給する親コンポーネント側は以上です。

文字列キーを使用した場合のinject

データを受け取るにはinject関数では、ジェネリックで明示的に型の指定をする必要があります。
明示しないとunknown型になってしまいます。

const useKey1 = inject<number>("key1")
const useKey2 = inject<string>("key2")

InjectionKeyを使用する方法

InjectionKeyを使用した場合のprovide

InjectionKeyというものをimportすることで、キー自体に型の定義を含めることが可能です。
次のように定義して利用します。

import { InjectionKey } from "vue"
const key: InjectionKey<渡したいデータの型> = Symbol()

コードの通り、InjectionKeyの正体はSymbol型です。

キーにデータの型が含まれているので、provideには型指定がいりません。

provide(key, hoge)

InjectionKeyはSymbol型なので、このキーは唯一無二です。
つまり、別ファイルで同じコードを記述したところで別のものとなってしまいます。
よって、InjectionKeyを定義したファイルからexportして利用します。

script setup構文を用いている場合、exportが使えないので次のように分割して記述する必要があります。

<script lang="ts">
import { InjectionKey } from "vue";
export const key: InjectionKey<string> = Symbol()
</script>

<script setup lang="ts">
import { provide } from "vue"
provide(key, "hoge")
</script

InjectionKeyを使用した場合のinject

データを受け取るコンポーネントでは、親でexportしたキーをimportしてinject関数に渡すだけです。
キーに型定義が含まれているので、inject関数で型を定義する必要はありません。
よって、シンプルに次のようにして記述します。

const useKey = inject(key)

どちらのキーを利用すれば良いか

文字列キーよりもInjectionKeyを利用したほうがTypeScriptの恩恵を受けることができますし、バグの少ないコードが書けます。

というのも、文字列キーを利用した場合は次の点が問題になるからです。

  • 同名のキーをすでに指定していもエラーは出ずに上書きされる
  • injectでキーを利用するとき、親で指定したキー名を開発者がタイプミスしても気づかない
  • injectで渡されるデータの型を、開発者が知る必要がある。

InjectionKeyを利用すれば、キーはimportして利用するので(エディタの機能によって)キーのタイプミスに気づくことが可能です。
また、キー自体に型の定義が含まれているので、injectを利用する側は型の定義を予め知る必要がありません。

使い方について補足

リアクティブなデータは子孫コンポーネントで変更しない

マストではありませんが、極力変更しないことが推奨されています。
どうしても変更する必要がある場合、次のように、リアクティブなデータを変更する責務を持つメソッドを親から提供します。

親コンポーネント

// Component A
<script lang="ts">
import { InjectionKey, Ref } from "vue";
export const numKey: InjectionKey<Ref<number>> = Symbol('num')
export const updateNumKey: InjectionKey<(n: number) => void> = Symbol('updateNum')
</script>

<script setup lang="ts">
import ComponentB from "./ComponentB.vue"
import { provide, ref } from "vue"

//リアクティブデータ
const num = ref(0)

//リアクティブデータの更新メソッド
const updateNum = (n: number) => num.value = n

provide(numKey, num) // リアクティブデータのprovide
provide(updateNumKey, updateNum) //メソッド自体をprovide
</script>

子孫コンポーネント

// Component B
<script setup lang="ts">
import { numKey, updateNumKey }  from "./ComponentA.vue"
import { inject } from "vue"

const useNum = inject(numKey)
const useUpdateKey = inject(updateNumKey)

// 何らかのイベントでリアクティブデータの変更
const onClick = () => useUpdateKey?.(2)
</script>

また、子孫での変更を防ぐために、provideする際にはreadonlyをつけることが推奨されています。

import { readonly, provide } from "vue" 
provide(key, readonly(hoge))

まとめ

Vue3において、ProvideとInjectをComposition APIとTypeScriptで解説しました。また、provideでのキー指定の方法は2種類あり、InjectionKeyを利用したほうがよりTypeScriptの恩恵を受けて記述できることも説明しました。このようにProvideとInjectでpropsのバケツリレーを簡単に防ぐことが可能です。かといって多様は禁物で、きちんとコンポーネントや状態管理の設計をして、コンポーネントの責務を考えた上で利用すべきです。