[Vue入門]Provide / Inject

Prop の過剰的な使用(Prop Drilling)

通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合、props を使用します。しかし、大きなコンポーネントツリーがあり、深くネスト化されたコンポーネントが遠い場合に親元から複数先のコンポーネントにデータを飛ばしたいシナリオを想定してください。

propsのやり方だと、親コンポーネントからつながるすべてのcomponentに同じ prop を渡さなければなりません:

<Footer> コンポーネントは親から受け取るの props を全く使わない場合でもデータの渡し役として、コンポーネントを記載する必要があります。それから<DeepChild> が<Footer>コンポーネントからこのデータにアクセスできるようにしてやっと<Root>コンポーネントのデータを受け取ることができるようになります。これでは、コードが煩雑になり、デバッグの作業も面倒になりますね。

このPropsを何階層も下に投げる作業を省くには provide と inject を使うことで解決できます。親コンポーネントは、そのすべての子コンポーネントに対して 依存関係を提供するプロバイダー (dependency provider) として機能することができます。子ツリー内のどのコンポーネントも、その深さに関係なく、親チェーン内の上位コンポーネントが提供する依存性を注入 (inject) することができます。

Provide

親コンポーネントから子コンポーネントにデータを提供するには provide() 関数を使います:

<script setup>
import { provide } from 'vue'

provide(/* key */ 'message', /* value */ 'hello!')
</script>

<script setup> を使わない場合、setup() 内で provide() が同期的に呼び出されていることを確認してください:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* key */ 'message', /* value */ 'hello!')
  }
}

provide() 関数は 2 つの引数を受け付けます。第 1 引数はインジェクションキーと呼ばれ、文字列または Symbol となります。provideで使用するデータを投げるために使うニックネームのようなものですね。

このインジェクションキーは、子のコンポーネントが、インジェクション(注入)に必要な値を探すのに使われます。1 つのコンポーネントが異なる値を提供するために、異なるインジェクションキーで provide() を複数回呼び出すことができます。

第 2 引数は提供される値です。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

リアクティブな値を提供することで、提供された値を使用する子孫コンポーネントが、プロバイダーコンポーネントとのリアクティブな接続を確立することができます。

アプリケーションレベルの Provide

コンポーネント内だけでなく、アプリケーションレベルでデータを提供することも可能です:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

アプリケーションレベルの Provide は、アプリケーションでレンダリングされるすべてのコンポーネントで利用可能です。これは特にプラグインを書くときに便利です。プラグインは通常、コンポーネントを使ってデータを提供することができないからです。

Inject

親コンポーネントが提供するデータを注入するには inject() 関数を使用します:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

提供された値が ref である場合、そのまま注入され、自動的にアンラップされることはありません。これにより、インジェクターコンポーネントはプロバイダーコンポーネントとのリアクティビティの接続を保持することができます。

繰り返しますが、もし <script setup> を使用しないのであれば、inject() は setup() の内部でのみ同期的に呼び出す必要があります:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

インジェクションのデフォルト値

デフォルトでは、inject は注入されるキーが親チェーンのどこかで提供されることを想定しています。キーが提供されていない場合、実行時が出ます。

インジェクトされたプロパティをオプションのプロバイダーで動作させたい場合は、props と同様にデフォルト値を宣言する必要があります:

// もし "message" にマッチするデータがなかった場合は、
// `value` は "default value" になります
const value = inject('message', 'default value')

場合によっては、関数を呼び出したり、新しいクラスをインスタンス化したりして、デフォルト値を作成する必要があるかもしれません。オプションの値が使用されないケースで不要な計算や副作用を避けるために、デフォルト値を作成するためのファクトリー関数を使用することができます:

const value = inject('key', () => new ExpensiveClass())

リアクティビティと共に利用する

リアクティブな値を provide / inject する場合、可能な限り、リアクティブな状態への変更を provider の内部で維持することが推奨されます。これは、提供されるステートとその可能な変更が同じコンポーネントに配置されることを保証し、将来のメンテナンスをより容易にするためです。

インジェクターコンポーネントからデータを更新する必要がある場合があります。そのような場合は、状態の変更を担当する関数を使うことをおすすめします:

<!-- プロバイダーコンポーネント内部 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- インジェクターコンポーネント内部 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最後に、provide を通して渡されたデータが注入されたコンポーネントによって変更されないようにしたい場合は、提供された値を readonly() でラップすることができます。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

シンボルキーと共に利用する

今までの例では、文字列のインジェクションキーを使っていました。もしあなたが多くの依存関係を提供するプロバイダーを持つ大規模なアプリケーションで作業していたり、他の開発者が使用する予定のコンポーネントを作成している場合は、名前の重複を避けるためにシンボルインジェクションキーを使用するのがベストです。

シンボルは専用のファイルに書き出しておくことをおすすめします:

// keys.js
export const myInjectionKey = Symbol()
// プロバイダーコンポーネント内
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, {
  /* 提供するデータ */
})
// インジェクターコンポーネント内
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)