Vue 3 の `provide` と `inject`:コンポーネント間のレベルを跨いだ通信
provide と inject は、Vue 3 の レベル間通信 API です。親コンポーネントがデータを提供し、子コンポーネント(階層の深さに関係なく)はそれを注入して利用できます。これにより、「プロパティドリリング」という課題が解決されます。
Vue 3のprovideはリアクティブデータ(ref / reactive)をサポートしており、子コンポーネントは注入されると自動的にリアクティブになります。このレッスンでは、この仕組みと5つの実用的なユースケースを習得できます。
1. 学習内容
provide/inject基本構文- インジェクトキーとしてのシンボル型
- リアクティブ・プロバイド(参照 / リアクティブ)
provideを変更する 2 つの方法- プロップス、イベントバス、Piniaとの比較
- 5つの主要な実運用シナリオ(テーマ/ユーザー/i18n)
- 「provide」および「inject」に関連する3つのアンチパターン
2. 5つのレベルにわたって「テーマカラー」をプロップドリルで掘り進める悪夢
(1) 課題:アプリ → レイアウト → ヘッダー → ユーザーメニュー → ボタン、5層のパススルー
アリスの管理ダッシュボードでは、テーマの切り替えに対応する必要がありました:
VUE
<!-- ❌ The "Flip" Version: 5-Layer prop drilling -->
<!-- App.vue -->
<Layout :theme="theme" :user="user" :locale="locale">
<router-view />
</Layout>
<!-- Layout.vue -->
<Header :theme="theme" :user="user" :locale="locale" />
<!-- Header.vue -->
<UserMenu :theme="theme" :user="user" :locale="locale" />
<!-- UserMenu.vue -->
<ThemeButton :theme="theme" />
<LocaleSelector :locale="locale" />
プロダクトマネージャーのチャーリーが、さらに3つの設定を追加しました:
「アリス、サイドバーの折りたたみ設定、通知設定、機能フラグの設定を追加する必要がある。5つのコンポーネントに8つのプロパティを渡すことになる。これではメンテナンスが不可能だ。」
(2) Vueによる提供/注入の仕組み:親要素に注入し、子要素が受け取る
VUE
<!-- App.vue Ancestors -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('light')
const user = ref({ name: 'Alice' })
const locale = ref('zh-CN')
// ✅ Once provide,All descendants may inject
provide('theme', theme)
provide('user', user)
provide('locale', locale)
</script>
<!-- UserMenu.vue Any descendant at any depth -->
<script setup>
import { inject } from 'vue'
// ✅ Get it directly, No props passthrough needed
const theme = inject('theme')
const user = inject('user')
const locale = inject('locale')
</script>
<template>
<button :class="theme">Toggle {{ user.name }}</button>
<p>{{ locale }}</p>
</template>
5段階のネスト → provide 1行 + inject 1行。
(3) 収益
提供/注入後:
- コンポーネントのネスト階層:無制限(ツリーのどのレベルでもインジェクションが可能)
- プロップの数:5以上 → 0(渡されなくなった)
- 新しい構成:必要なのは
provide1つだけです - 保守性:親コンポーネントがグローバル状態を一元的に管理する
3. provide および inject の基本的な構文
(1) 入力データを指定する
VUE
<!-- App.vue Ancestor Components -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('light')
const user = { name: 'Alice' }
// 1. Inject a static value
provide('appName', 'My Admin')
// 2. Injecting Responsive Data(ref)
provide('theme', theme)
// 3. Injecting Responsive Data(reactive)
provide('user', user)
// 4. Injection Methods
provide('updateUser', (newUser) => {
user.value = newUser
})
</script>
(2) injectがデータを受信する
VUE
<!-- Child.vue Any descendant component -->
<script setup>
import { inject } from 'vue'
// 1. Basic Reception
const theme = inject('theme')
// 2. With default values(My ancestors are no longer with us provide When to Use)
const theme = inject('theme', 'light')
// 3. Include factory function defaults
const theme = inject('theme', () => 'light')
// 4. Type Assertion(TypeScript)
const theme = inject<string>('theme', 'light')
</script>
(3) 完全な例
VUE
<!-- Ancestor.vue -->
<script setup>
import { ref, provide } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })
provide('count', count) // ref
provide('user', user) // reactive
provide('config', { theme: 'dark' }) // Object
provide('reset', () => { count.value = 0 }) // Methods
</script>
<!-- DeepChild.vue(Any depth) -->
<script setup>
import { inject } from 'vue'
const count = inject('count') // Responsive
const user = inject('user') // Responsive
const config = inject('config') // Ordinary Object
const reset = inject('reset') // Function
// Edit
function increment() {
count.value++ // ✅ Ancestor components will also respond
}
</script>
4. シンボル型をインジェクトキーとして使用する場合
(1) なぜSymbolを使うのか?
文字列キーは競合が生じやすい(同じキーを持つ複数の provide ステートメントがあると、互いに上書きされてしまう)。シンボルは一意であるため、競合を防ぐことができる。
VUE
<!-- keys.js - Centrally manage all provide key -->
<script>
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CONFIG_KEY = Symbol('config')
</script>
VUE
<!-- Ancestor.vue Ancestor Components -->
<script setup>
import { provide, ref } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY } from './keys'
provide(THEME_KEY, ref('light'))
provide(USER_KEY, ref({ name: 'Alice' }))
provide(LOCALE_KEY, ref('zh-CN'))
</script>
VUE
<!-- DeepChild.vue Any descendants -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './keys'
// ✅ Type Safety:Must use Symbol talent inject
const theme = inject(THEME_KEY)
const user = inject(USER_KEY)
</script>
(2) 5つの主な利点
| 利点 | 説明 |
|---|---|
| 一意性 | シンボルはグローバルに一意であり、競合は発生しない |
| 型安全性 | TypeScript は正確に型を推論できる |
| 一元管理 | すべての鍵は単一のファイルに保存される |
| リファクタリングに適している | キーを変更しても他のキーには影響しない |
| 可読性 | キー名を見るだけで、その用途がわかる |
5. レスポンシブ provide の詳細な解説
(1) 「provide」の3つのモード
VUE
<!-- Ancestor.vue -->
<script setup>
import { ref, reactive, provide } from 'vue'
// 1. Inject ref(Responsive)
const count = ref(0)
provide('count', count)
// 2. Inject reactive(Responsive)
const user = reactive({ name: 'Alice' })
provide('user', user)
// 3. Injecting Read-Only Objects(Non-responsive)
const config = { theme: 'dark' }
provide('config', config)
</script>
(2) 変更を行う2つの方法
VUE
<!-- Method 1:Directly Modify Ancestors(Recommendations) -->
<!-- Ancestor.vue -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('light')
provide('theme', theme)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<!-- Method 2:Through their descendants inject Edit Citation -->
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
function toggleTheme() {
theme.value = 'dark' // ✅ The change is ref,Both the ancestors and other descendants responded
}
</script>
(3) 5つの主要な対応シナリオ
| シナリオ | 用途 |
|---|---|
| テーマを切り替える | ref + provide |
| ユーザーログイン | reactive + provide |
| i18n ロケール | 参照 + 提供 + 切り替え方法 |
| グローバル読み込み | ref(false) + 提供 |
| グローバルメッセージ通知 | リアクティブ + 提供 |
6. 実社会における5つの主要なシナリオ
(1) シーン1:話題の切り替え
JS
// keys.js
export const THEME_KEY = Symbol('theme')
VUE
<!-- App.vue Ancestors -->
<script setup>
import { ref, provide } from 'vue'
import { THEME_KEY } from './keys'
const theme = ref('light')
provide(THEME_KEY, theme)
</script>
<!-- ThemeButton.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './keys'
const theme = inject(THEME_KEY)
</script>
<template>
<button @click="theme.value = theme === 'light' ? 'dark' : 'light'">
{{ theme === 'light' ? '🌞' : '🌙' }}
</button>
</template>
(2) シナリオ 2:ユーザー認証
JS
// keys.js
export const AUTH_KEY = Symbol('auth')
VUE
<!-- App.vue -->
<script setup>
import { reactive, provide } from 'vue'
import { AUTH_KEY } from './keys'
const auth = reactive({
user: null,
isLoggedIn: false,
login(credentials) { /* API call */ },
logout() { this.user = null; this.isLoggedIn = false }
})
provide(AUTH_KEY, auth)
</script>
<!-- UserMenu.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { AUTH_KEY } from './keys'
const auth = inject(AUTH_KEY)
</script>
<template>
<div v-if="auth.isLoggedIn">{{ auth.user.name }}</div>
<button v-else @click="auth.login(creds)">Login</button>
</template>
(3) シナリオ 3: i18n
JS
// keys.js
export const I18N_KEY = Symbol('i18n')
VUE
<!-- App.vue -->
<script setup>
import { ref, provide, computed } from 'vue'
import { I18N_KEY } from './keys'
const messages = {
'zh-CN': { hello: 'Hello', welcome: 'Welcome' },
'en-US': { hello: 'Hello', welcome: 'Welcome' }
}
const locale = ref('zh-CN')
const t = computed(() => (key) => messages[locale.value][key])
provide(I18N_KEY, { locale, t })
</script>
<!-- Hello.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { I18N_KEY } from './keys'
const { t } = inject(I18N_KEY)
</script>
<template>
<h1>{{ t('hello') }}, {{ t('welcome') }}</h1>
</template>
(4) シナリオ4:グローバルロード
VUE
<!-- App.vue -->
<script setup>
import { ref, provide } from 'vue'
const loading = ref(false)
provide('loading', loading)
</script>
<!-- AnyComponent.vue Descendants -->
<script setup>
import { inject } from 'vue'
const loading = inject('loading')
async function fetchData() {
loading.value = true
await fetch('/api/data')
loading.value = false
}
</script>
<template>
<button @click="fetchData">Refresh</button>
</template>
(5) シナリオ 5:テーマ + ビジネスデータ
VUE
<!-- App.vue -->
<script setup>
import { ref, reactive, provide } from 'vue'
const appState = reactive({
theme: 'light',
user: { name: 'Alice' },
permissions: ['read', 'write'],
config: { sidebar: true }
})
provide('appState', appState)
</script>
7. provide / inject とその他のアプローチの比較
(1) 5つのコミュニケーション手法の比較
| 側面 | プロパティ/エミット | 提供/注入 | イベントバス | Pinia | mitt |
|---|---|---|---|---|---|
| 父と息子の文通 | ⭐⭐⭐⭐⭐ | - | - | - | - |
| レベル横断 | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Brothers Communications | ❌(保護者の仲介が必要) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| レスポンシブ | ✅ | ✅ | ❌ (手動) | ✅ | ❌ |
| 型安全性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| DevTools | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 習得の難易度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
(2) 5つの主要な選定シナリオ
| シナリオ | 推奨される解決策 |
|---|---|
| 親子間のコミュニケーション | props/emit |
| レベルをまたぐグローバル状態(テーマ/認証/i18n) | 提供/注入 |
| 中規模~大規模アプリケーションにおけるグローバル状態 | Pinia (フェーズ 4.2) |
| シンプルなイベントバス | mitt / tiny-emitter |
| 複雑なビジネス状態の管理 | Pinia + Vue Router |
8. 完全な例:ECサイトのバックエンドでのテーマの切り替え
(1) ▶ サンプル:. 鍵の一元管理
JS
// src/keys.js
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CART_KEY = Symbol('cart')
(2) ▶ サンプル:. App.vue—1回限り provide
VUE
<!-- src/App.vue -->
<script setup>
import { ref, reactive, provide, readonly } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY, CART_KEY } from './keys'
// Topic
const theme = ref('light')
function setTheme(newTheme) {
theme.value = newTheme
localStorage.setItem('theme', newTheme)
}
// User
const user = reactive({
id: 1,
name: 'Alice',
role: 'admin'
})
// Internationalization
const locale = ref('zh-CN')
const messages = {
'zh-CN': { home: 'Home', cart: 'Shopping Cart' },
'en-US': { home: 'Home', cart: 'Cart' }
}
const t = (key) => messages[locale.value][key]
// Shopping Cart
const cart = reactive({ items: [], total: 0 })
// Disposable provide 4 Global state
provide(THEME_KEY, { theme, setTheme })
provide(USER_KEY, { user })
provide(LOCALE_KEY, { locale, t })
provide(CART_KEY, { cart })
</script>
<template>
<router-view />
</template>
(3) ▶ サンプル:. 子孫による使用(5つの使い方)
VUE
<!-- DeepChild.vue Any depth -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY, CART_KEY } from './keys'
// 1. Deconstruction
const { theme, setTheme } = inject(THEME_KEY)
const { user } = inject(USER_KEY)
const { locale, t } = inject(LOCALE_KEY)
const { cart } = inject(CART_KEY)
</script>
<template>
<div :class="['app', theme]">
<p>{{ user.name }} ({{ user.role }})</p>
<p>{{ t('home') }} | {{ t('cart') }}</p>
<p>{{ cart.items.length }} items, ${{ cart.total }}</p>
<button @click="setTheme('dark')">Dark Mode</button>
</div>
</template>
(4) ▶ サンプル:. シンボルインジェクションキーの5つの主な利点の実演
JS
// src/keys.js
export const THEME_KEY = Symbol('theme') // The Only One
export const ANOTHER_THEME_KEY = Symbol('theme') // Also the only one(Different Symbol)
VUE
<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
import { THEME_KEY, ANOTHER_THEME_KEY } from './keys'
const theme1 = ref('light')
const theme2 = ref('dark')
// ✅ There will be no conflict:Different Symbol Even if the names are the same
provide(THEME_KEY, theme1)
provide(ANOTHER_THEME_KEY, theme2)
</script>
(5) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| 文字列キーの競合 | 上書き | シンボルを使用 |
| 祖先以外の提供 | 未定義 | コンポーネント階層を確認 |
| 読み取り専用プロパティの変更 | 警告 | 親オブジェクトを変更しました。子オブジェクトは読み取り専用です |
| デフォルト値を無視 | 未定義 | inject(key, defaultValue) |
provide ステートメントが多数ある |
保守が困難 | keys.js に集中している |
(6) ▶ サンプル:. 5つの主要なパフォーマンス比較
| モード | 性能 | 適用対象 |
|---|---|---|
provide('key', value) |
⭐⭐⭐⭐⭐ | 固定値 |
provide('key', ref) |
⭐⭐⭐⭐⭐ | レスポンシブ評価 |
provide('key', reactive) |
⭐⭐⭐⭐ | 複雑なオブジェクト |
provide('key', computed) |
⭐⭐⭐⭐⭐ | 算出値 |
provide('key', function) |
⭐⭐⭐⭐ | 使い方 |
❓ よくある質問
Q
provide と props、どちらを使うべきですか?A 親コンポーネントと子コンポーネント間の直接通信(明示的かつ型安全)には
props を使用してください。プロパティドリリングを回避するために、レベルをまたぐ通信(3レベル以上)には provide を使用してください。大規模なアプリケーションにおけるグローバル状態の管理には、Pinia(Phase 4.2)を使用してください。Q 提供されるデータはリアクティブですか?
A 提供されるデータによって異なります。
provide('key', ref) リアクティブ、provide('key', {a: 1}) 非リアクティブ(通常のオブジェクト)。provide ref または provide reactive の使用をお勧めします。Q
provide は setup の最上位レベルで呼び出す必要がありますか?A はい。
provide は <script setup> の最上位レベルで呼び出す必要があります(ライフサイクルフック内での呼び出しは許可されていません)。そうしないと、provide は有効になりません。Q
inject の後に provide の値を変更することはできますか?A
ref.value を変更することは可能ですが、親コンポーネント内でのみ(provide メソッドを使用して)変更することをお勧めします。ref を直接変更すると、データの流れが不明確になる可能性があります。Q シンボルキーはどこで定義されていますか?
A すべてのシンボルをエクスポートする別のファイル
src/keys.js です。使用されるすべてのコンポーネントは、このファイルからインポートされます。Q
provide/inject と Pinia のどちらを選べばよいですか?A 小規模なプロジェクト(グローバル状態が 5 つ未満)の場合は、
provide/inject で十分です。中規模から大規模なプロジェクト(グローバル状態が 10 以上)の場合は、Pinia の方が DevTools への対応が充実しているため、Pinia を使用してください。Q
provide を含むコンポーネントがアンマウントされるとどうなりますか?A すべての子孫コンポーネントもアンマウントされます(Vue コンポーネントツリーのライフサイクル)。
inject は再び上方向の祖先を検索します。📖 まとめ
provideおよびinjectは、Vue 3 のクロスレベル通信 API です- provide(祖先)、inject(任意の子孫)
provideの5つのタイプ:定数値/参照/リアクティブ/計算済み/関数- シンボルキー:競合を避けるため、keys.js で一元管理する
- 5つの実践的なシナリオ:テーマ/認証/i18n/読み込み/ビジネスデータ
- propsとの比較:親子関係にはpropsを使用し、レベル間の通信には
provideを使用する - Piniaとの比較:小規模なプロジェクトにはProvideを、大規模なプロジェクトにはPiniaを使用する
📝 練習問題
(1) 基礎問題(難易度:⭐)
簡単なテーマ切り替え機能を実装する:
- keys.js で THEME_KEY を定義する
- App.vue では、テーマ(参照)と setTheme メソッドが提供されています
- ThemeButton.vue:テーマの注入 + setTheme
- ボタンをクリックして、ライトモードとダークモードを切り替えてください
(2) 上級問題(難易度:⭐⭐)
ユーザー認証を実装する(提供/注入):
- AUTH_KEY インジェクション:user、login、logout メソッド
- App.vue で user = null と初期化される
- LoginPage.vue で
login(creds)を呼び出し、ログインをシミュレートする - Header.vue は、isLoggedIn の値に応じて異なる UI を表示します
(3) チャレンジ問題(難易度:⭐⭐⭐)
eコマースのバックエンド向けに、包括的なグローバル状態管理システムを実装する:
- 4つのキー:THEME / USER / CART / LOCALE
- App.vue は 4 つのグローバル状態を一度に提供します
- 5つのコンポーネントを異なるレベル(ヘッダー/サイドバー/商品一覧/カート/フッター)で使用すること
- コンポーネント間の連携:ヘッダーでログアウトがトリガーされると、サイドバーが自動的に更新される
- テーマの切り替え + 国際化(i18n)の切り替え + ショッピングカートへの商品の追加・削除
- 強型付けのためのTypeScriptの使用
- 5 パターンの提供(Value/ref/reactive/computed/function)



