Vue 3 の provide/inject が reactive で更新されない問題の解決法

Vue 3 の provide / inject は、親コンポーネントから子コンポーネントへ値を渡すための仕組みです。コンポーネントツリーをまたいで状態を共有できるため、グローバルストアを使うほどではない軽量なデータ共有に便利です。しかし、開発の現場では「provide した値を更新しても、inject 側が反映されない」「リアクティブに動かない」といった問題が頻繁に報告されます。

本記事では、その原因と正しいリアクティブ化の方法を、実際のコードを交えながら解説します。

1. provide/inject の基本構造

provide / inject は親子間で値を共有する最もシンプルな手段です。基本的な例を以下に示します。

<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'
const message = ref('Hello from parent')
provide('sharedMessage', message)
</script>

<template>
  <ChildComponent />
</template>
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const sharedMessage = inject('sharedMessage')
</script>

<template>
  <p>{{ sharedMessage }}</p>
</template>

この構成では、親の messageref であるため、inject 側でもリアクティブに反映されます。しかし、もし単純なオブジェクトやプリミティブ値を provide した場合、更新がリアクティブに伝わらないことがあります。

2. なぜ reactive にならないのか

provide / inject で値がリアクティブに更新されない最大の理由は、「Vue のリアクティブシステムが inject 先までトラッキングしていない」ためです。具体的には、provide でオブジェクトをそのまま渡すと、Vue はその変更を検知できません。

たとえば次のコードを見てみましょう。

<!-- NG 例 -->
<script setup>
import { provide } from 'vue'
const state = { count: 0 }
provide('counter', state)
</script>

この場合、state.count++ としても、子コンポーネントでは再レンダーが発生しません。Vue は plain object の変更を監視していないためです。

3. 正しい reactive 化の方法

リアクティブに共有したい場合は、reactive() または ref() を使ってラップする必要があります。以下が正しい例です。

<!-- Parent.vue -->
<script setup>
import { reactive, provide } from 'vue'
const state = reactive({ count: 0 })
provide('counter', state)
</script>

<template>
  <button @click="state.count++">+</button>
  <Child />
</template>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
const counter = inject('counter')
</script>

<template>
  <p>Count: {{ counter.count }}</p>
</template>

これで、親で state.count++ した際に子の表示も自動的に更新されます。
重要なのは、リアクティブな型を provide することです。reactive / ref のいずれも可能ですが、ref を使う場合は .value を考慮する必要があります。

ref を使う場合の例

<script setup>
import { ref, provide } from 'vue'
const message = ref('Hello')
provide('msg', message)
</script>
<script setup>
import { inject } from 'vue'
const msg = inject('msg')
</script>

<template>
  <p>{{ msg }}</p> <!-- Vue が自動的に .value を解決 -->
</template>

4. inject 側で値が undefined になるケース

もう一つよくある落とし穴は、inject のタイミングが親の provide より先に評価されてしまう場合です。これは setup() の実行順序や動的コンポーネントのマウント順によって発生します。
そのようなケースでは、inject('key', defaultValue) のようにデフォルト値を設定しておくことで、安全にアクセスできます。

<script setup>
import { inject } from 'vue'
const counter = inject('counter', { count: 0 })
</script>

また、Composition API の useState(Nuxt 3 など)や pinia と併用している場合、provide のタイミングが異なることもあるため、onMounted 内で inject を確認するのも有効です。

5. 実務でのトラブルとパターン別対処

実際の開発現場では、provide/inject のリアクティブ問題は次のようなパターンで発生します。

  • provide に reactive / ref を使わず、単なるオブジェクトを渡している
  • inject 側で分割代入をして reactivity を失っている
  • Composition API と Options API が混在しており、provide の順序が不安定
  • SSR や Nuxt の環境下で hydration mismatch が起き、状態が同期しない

たとえば次のようなコードは NG です。

<script setup>
const state = reactive({ count: 0 })
provide('count', state.count) // プリミティブを渡してしまう
</script>

これではリアクティブなオブジェクトではなく、数値そのものがコピーされるため、更新が伝わりません。
この場合は provide('count', state) とし、inject 側で count.count にアクセスする必要があります。

6. ベストプラクティスと代替案

Vue チームも公式に「provide/inject は主にライブラリやプラグイン実装での利用を想定しており、状態管理用途には pinia などを使うべき」と述べています。
しかし、小規模なアプリや一部機能における軽量共有であれば、以下のような設計指針を守ることで十分に安定した挙動を得られます。

  • 常に reactive または ref を provide する
  • inject した値を分割代入せず、そのまま参照する
  • SSR 環境では provide タイミングを確認する
  • 型定義(TypeScript)では InjectionKey を明示的に指定する
  • グローバル状態共有が必要なら pinia への移行を検討する

InjectionKey の例

// keys.ts
import type { InjectionKey } from 'vue'
export const CounterKey: InjectionKey<{ count: number }> = Symbol('CounterKey')
<script setup lang="ts">
import { provide, reactive } from 'vue'
import { CounterKey } from './keys'
const state = reactive({ count: 0 })
provide(CounterKey, state)
</script>
<script setup lang="ts">
import { inject } from 'vue'
import { CounterKey } from './keys'
const counter = inject(CounterKey)
</script>

こうすることで、型安全かつ確実なリアクティブ共有が可能になります。

まとめ

provide / inject が reactive に動かないのは Vue の仕組み上の仕様であり、バグではありません。リアクティブなデータを共有したい場合は、必ず reactive または ref を使って包み、プリミティブ値を直接渡さないようにしましょう。また、型定義や SSR の挙動を意識することで、より堅牢なコンポーネント間通信を実現できます。

参考

タイトルとURLをコピーしました