親から子へ、さらにその子へ、と何階層も値を渡していくと、途中のコンポーネントが、自分では使わないpropsを中継するだけになります。これを「prop drilling」と呼びます。provideとinjectは、この中継をなくし、祖先から離れた子孫へ、直接値を渡す仕組みです。この記事では、Vue 3を前提に、基本から、安全な設計までを動くコードで整理します。
provide/injectとは
祖先のコンポーネントがprovideで値を提供し、子孫のコンポーネントがinjectで受け取ります。「親子間」に限らず、祖先から、何階層下の子孫にも届きます。複数の祖先が同じキーを提供している場合は、一番近い祖先の値が使われます。
<!-- 祖先コンポーネント(Provider) -->
<script setup>
import { provide } from 'vue'
// 第1引数がキー、第2引数が値
provide('user', '山田太郎')
</script>
<!-- 子孫コンポーネント(Injector):何階層下でもよい -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<p>こんにちは、{{ user }}さん</p>
</template>
Options APIでは、provide()とinjectのオプションで書きます。
// Options API の場合
export default {
// 祖先
provide() {
return { message: 'hello!' }
}
}
export default {
// 子孫
inject: ['message']
}
注意として、provide()とinject()は、setup()(<script setup>)の中で、同期的に呼ぶ必要があります。
キーにはSymbolを使う
文字列のキー('user'など)は、アプリが大きくなると衝突するおそれがあります。公式は、Symbolを使い、キーを別ファイルにまとめる書き方を勧めています。
// keys.js:キーを1か所にまとめて、Symbol で衝突を防ぐ
export const themeKey = Symbol('theme')
実用例:テーマの切り替え
離れた場所にあるボタンから、アプリ全体のテーマを切り替える例です。ポイントは、値はreadonlyで提供し、変更は更新用の関数にまとめることです。変更が必ずプロバイダー側を通るので、誰がいつ値を変えたのかを追いやすくなります。
<!-- 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>
<!-- 深い階層にある 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のオブジェクトを丸ごと提供すると、リアクティブには動きますが、どの子孫からでも直接書き換えられます。小さなアプリでは気になりませんが、規模が大きくなると、値がどこで変わったのか追えなくなります。
<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を渡します。デフォルトがなく、キーが提供されていないと、実行時に警告が出ます。
<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を使います。プラグインを書くときに便利です。
// 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を使った状態管理の始め方で解説しています。
関連記事
v-modelでコンポーネントとやり取りする方法はv-modelの仕組みを完全理解- 派生した値はcomputedプロパティで動的なデータ処理を行う方法
- ダークモードの実装はダークモード切り替えの実装
よくある質問(FAQ)
readonly()で包むと、直接の書き換えを防げます。provide/inject、アプリ全体で、どこからでも使う状態はPiniaが向いています。まとめ
provide/injectは、prop drillingを避けて、祖先から子孫へ値を直接届ける仕組みです。キーにはSymbolを使い、値はreadonlyで、変更は更新用の関数で提供すると、安全に使えます。データの流れが見えにくくなる性質があるので、テーマや設定のような用途に絞って使ってください。

