【Vue.js】provide/injectの仕組みと応用例|親子コンポーネント間の依存関係を整理する設計

【Vue.js】provide/injectの仕組みと応用例|親子コンポーネント間の依存関係を整理する設計 Vue.js

親から子へ、さらにその子へ、と何階層も値を渡していくと、途中のコンポーネントが、自分では使わないpropsを中継するだけになります。これを「prop drilling」と呼びます。provideとinjectは、この中継をなくし、祖先から離れた子孫へ、直接値を渡す仕組みです。この記事では、Vue 3を前提に、基本から、安全な設計までを動くコードで整理します。

スポンサーリンク

provide/injectとは

祖先のコンポーネントがprovideで値を提供し、子孫のコンポーネントがinjectで受け取ります。「親子間」に限らず、祖先から、何階層下の子孫にも届きます。複数の祖先が同じキーを提供している場合は、一番近い祖先の値が使われます。

Vue:祖先で提供する
<!-- 祖先コンポーネント(Provider) -->
<script setup>
import { provide } from 'vue'

// 第1引数がキー、第2引数が値
provide('user', '山田太郎')
</script>
Vue:子孫で受け取る
<!-- 子孫コンポーネント(Injector):何階層下でもよい -->
<script setup>
import { inject } from 'vue'

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

<template>
  <p>こんにちは、{{ user }}さん</p>
</template>

Options APIでは、provide()とinjectのオプションで書きます。

Options API:provideとinject
// Options API の場合
export default {
  // 祖先
  provide() {
    return { message: 'hello!' }
  }
}

export default {
  // 子孫
  inject: ['message']
}

注意として、provide()とinject()は、setup()(<script setup>)の中で、同期的に呼ぶ必要があります。

キーにはSymbolを使う

文字列のキー('user'など)は、アプリが大きくなると衝突するおそれがあります。公式は、Symbolを使い、キーを別ファイルにまとめる書き方を勧めています。

JavaScript:キーを1か所にまとめる
// keys.js:キーを1か所にまとめて、Symbol で衝突を防ぐ
export const themeKey = Symbol('theme')

実用例:テーマの切り替え

離れた場所にあるボタンから、アプリ全体のテーマを切り替える例です。ポイントは、値はreadonlyで提供し、変更は更新用の関数にまとめることです。変更が必ずプロバイダー側を通るので、誰がいつ値を変えたのかを追いやすくなります。

Vue:App.vue(値と更新関数を提供)
<!-- App.vue(Provider) -->
<script setup>
import { ref, readonly, provide } from 'vue'
import { themeKey } from './keys'

const theme = ref('light')

// 変更は、プロバイダーの中だけで行う
function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// 値は readonly で包み、更新用の関数を一緒に提供する
provide(themeKey, {
  theme: readonly(theme),
  toggleTheme
})
</script>

<template>
  <div :class="`app app--${theme}`">
    <slot />
  </div>
</template>
Vue:ThemeButton.vue(受け取って使う)
<!-- 深い階層にある ThemeButton.vue(Injector) -->
<script setup>
import { inject } from 'vue'
import { themeKey } from './keys'

// ref は自動でアンラップされないので、使うときは .value を付ける(テンプレート内は不要)
const { theme, toggleTheme } = inject(themeKey)
</script>

<template>
  <button type="button" @click="toggleTheme">
    現在のテーマ:{{ theme }}(クリックで切り替え)
  </button>
</template>

注入したrefは、自動ではアンラップされません。スクリプトの中では.valueが必要です。テンプレートの中では、通常のrefと同じく不要です。

避けたい書き方:reactiveをそのまま提供する

reactiveのオブジェクトを丸ごと提供すると、リアクティブには動きますが、どの子孫からでも直接書き換えられます。小さなアプリでは気になりませんが、規模が大きくなると、値がどこで変わったのか追えなくなります。

Vue:直接書き換えられてしまう例
<script setup>
import { reactive, provide } from 'vue'

const settings = reactive({ lang: 'ja', theme: 'dark' })

// NG:reactive をそのまま提供すると、どの子孫からでも書き換えられる
provide('settings', settings)
// 子孫側で settings.lang = 'en' とできてしまい、誰が変更したのか追えなくなる
</script>

デフォルト値と、提供されていない場合

祖先が値を提供していない場合に備えて、injectの第2引数に、デフォルト値を指定できます。重い初期化が必要な場合は、ファクトリ関数にして、第3引数にtrueを渡します。デフォルトがなく、キーが提供されていないと、実行時に警告が出ます。

Vue:injectのデフォルト値
<script setup>
import { inject } from 'vue'

// 提供されていない場合の、デフォルト値
const lang = inject('lang', 'ja')

// 重い初期化は、ファクトリ関数にして、第3引数に true を渡す
const heavy = inject('heavy', () => new ExpensiveClass(), true)

// デフォルトがなく、キーが提供されていないと、実行時に警告が出る
const missing = inject('not-provided')
</script>

アプリ全体に提供する:app.provide

アプリ内のすべてのコンポーネントに値を届けたい場合は、アプリのインスタンスのprovideを使います。プラグインを書くときに便利です。

JavaScript:app.provide
// main.js:アプリ全体に提供する(プラグインなどで便利)
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.provide('appName', 'My App')
app.mount('#app')

props・emit・Piniaとの使い分け

  • props / emit:親子の直接のやり取り。データの流れが一番分かりやすいので、まずはこれを使う。
  • provide / inject:特定のコンポーネントのツリーの中で共有したい設定や機能(テーマ、フォームのコンテキスト、UIライブラリの内部連携など)。
  • Pinia:アプリ全体で共有する状態(ログイン情報、カートなど)。ツリーに関係なく、どこからでも使いたいとき。

provide/injectは便利ですが、データの流れが暗黙になり、コードを読むだけでは、どこから値が来るのか分かりにくくなります。使いすぎず、用途を絞るのがコツです。それぞれの使い分けはコンポーネント間でデータをやりとりする方法、PiniaはPiniaを使った状態管理の始め方で解説しています。

関連記事

よくある質問(FAQ)

Qprovide/injectで、親子の間以外にも渡せますか?
A渡せます。祖先から、何階層下の子孫にも直接届きます。複数の祖先が同じキーを提供しているときは、一番近い祖先の値が使われます。
Qinjectした値を、子孫側で変更してもよいですか?
Aおすすめしません。変更はプロバイダー側にまとめ、更新用の関数を一緒に提供してください。値はreadonly()で包むと、直接の書き換えを防げます。
Q提供されていないキーをinjectするとどうなりますか?
Aデフォルト値を指定していなければ、実行時に警告が出ます。第2引数にデフォルト値を渡しておくと、警告が出ず、その値が使われます。
Qprovide/injectとPiniaは、どう使い分けますか?
A特定のコンポーネントのツリーの中で共有する設定や機能はprovide/inject、アプリ全体で、どこからでも使う状態はPiniaが向いています。

まとめ

provide/injectは、prop drillingを避けて、祖先から子孫へ値を直接届ける仕組みです。キーにはSymbolを使い、値はreadonlyで、変更は更新用の関数で提供すると、安全に使えます。データの流れが見えにくくなる性質があるので、テーマや設定のような用途に絞って使ってください。