Provide / Inject - 深い子孫へ文脈依存を直接渡す仕組み

ロードマップ: Vue.js学習ロードマップ

What(何についてか)

Provide / Inject は、祖先コンポーネントが依存値を提供し、その子孫コンポーネントが深さに関係なく直接受け取るための仕組みである。

props が親から子へ明示的に値を渡す契約であるのに対し、Provide / Inject はコンポーネントツリー内の文脈依存を共有する仕組みである。

Why(なぜ必要か)

大きなコンポーネントツリーでは、深い子孫コンポーネントが祖先の値を必要とする場合がある。 props だけでこれを実現すると、中間コンポーネントがその値を使わないにもかかわらず、受け渡しのためだけに props を宣言し続ける必要がある。

この問題を prop drilling と呼ぶ。

Provide / Inject により、

  • 中間コンポーネントの責務を汚さない
  • 深い子へ必要な依存だけを直接渡せる
  • theme, locale, form context, tabs state などの共有文脈を自然に表現できる

How(どう動くか)

1. Provide

祖先コンポーネントは provide(key, value) で依存を配布する。

<script setup>
import { provide } from 'vue'
 
provide('message', 'hello!')
</script>
  • 第1引数は injection key
  • 第2引数は提供する値
  • 値は文字列、オブジェクト、関数、ref など任意の型を取れる

ref を provide すると、inject 側と reactive な接続を保てる。

<script setup>
import { ref, provide } from 'vue'
 
const count = ref(0)
provide('count', count)
</script>

2. App-level Provide

コンポーネント単位ではなく、アプリ全体に対して依存を提供することもできる。

import { createApp } from 'vue'
 
const app = createApp({})
app.provide('message', 'hello!')

これは plugin やアプリ全体の config / service 共有に向く。

3. Inject

子孫コンポーネントは inject(key) で値を受け取る。

<script setup>
import { inject } from 'vue'
 
const message = inject('message')
</script>

同じ key を複数祖先が provide している場合は、最も近い祖先の値が使われる。

flowchart TD
  A["Provider A\nprovide('theme', 'light')"] --> B["Intermediate"]
  B --> C["Provider C\nprovide('theme', 'dark')"]
  C --> D["DeepChild\ninject('theme')"]

この場合、DeepChild が受け取るのは dark である。

4. Missing Inject と Default Values

provide が存在しない key を inject すると、通常は undefined になり warning が出る。 エラーとして即 throw されるわけではない。

const value = inject('missing')

optional な依存であれば default 値を指定できる。

const value = inject('message', 'default value')

生成コストの高い既定値は factory function として遅延生成できる。

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

5. KVS 的だが、ツリーにスコープされた DI である

Provide / Inject は key で値を登録・参照するため、KVS のように見える。 ただし本質は保存機構ではなく、ツリーにスコープされた dependency injection である。

特徴は以下。

  • グローバルではなく provider の subtree に閉じる
  • 同じ key が複数あれば nearest provider が勝つ
  • 値として state, service, function, config を共有できる

Working with Reactivity

1. ref はそのまま inject される

provide された値が ref の場合、inject 側では自動 unwrap されず、そのまま ref として受け取る。 これにより provider と injector の reactive なつながりが維持される。

2. mutation は provider 側に寄せる

Vue 公式は、共有 state の変更責務を可能な限り provider 側へ寄せることを推奨している。 state と mutation を同じ場所に閉じ込めたほうが、保守性が高くなるためである。

推奨パターンは、状態と更新関数を一緒に provide すること。

<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>

この設計により、inject 側は変更依頼を出すだけになり、shared state のルールが provider に集約される。

3. readonly による防御

inject 側から直接変更されたくない場合は readonly() を使う。

<script setup>
import { ref, provide, readonly } from 'vue'
 
const count = ref(0)
provide('read-only-count', readonly(count))
</script>

これは「子では読み取りだけが理想」という設計を API レベルで強制できる。

Symbol Keys

小規模では string key でもよいが、大規模アプリや共有部品、plugin では Symbol key が推奨される。 理由は string key の衝突回避である。

export const myInjectionKey = Symbol()
// provider
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
 
provide(myInjectionKey, {
  /* data */
})
// injector
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
 
const injected = inject(myInjectionKey)

magic string ではなく一意な識別子を共有することで、安全な dependency 解決ができる。

props / provide-inject / composable / store の役割分担

仕組み主用途
props / emits近い親子間の明示的なデータ入出力
provide / inject深い子へ親文脈を共有する局所的な DI
composableロジック再利用
store (Pinia など)アプリ横断の共有状態

Provide / Inject は万能ではなく、「深い子へ、親文脈を局所共有する」用途に最も向いている。

Options API → Composition API 差分(補足)

項目Options API(旧)Composition API(新)
provide / inject 呼び出し位置provide / inject option, setup()script setup 内で直接使用
reactivity の扱いdata() 値の provide は自動では reactive link にならない場合があるref / reactive をそのまま provide しやすい
ロジック共有との関係component option ベースで分散しやすいcomposable と役割を分離しやすい

Key Concepts

用語説明
prop drilling深い子へ値を渡すために中間コンポーネントが不要な props 中継をする問題
provide祖先が依存値を subtree に配布すること
inject子孫が祖先の依存値を key で受け取ること
app-level provideアプリ全体へ依存値を配布する仕組み
nearest provider wins同じ key が複数ある場合、最寄り祖先が優先される原則
default valueprovider 不在時の既定値
readonlyinject 側からの mutation を防ぐラッパ
Symbol keykey 衝突を防ぐための一意な識別子