Vue — provide / inject
テクノロジ系 / L02 component-design — コンポーネント設計
Vue — provide / inject
prop drilling 解消
App provide(theme)
└ Layout
└ Sidebar
└ Menu inject(theme) ← 中間を経由せず取得中間コンポーネントを経由せずに値を渡せる。
基本
<!-- App.vue (祖先) -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
provide("theme", theme);
</script>
<template>
<Layout />
</template><!-- DeepChild.vue -->
<script setup lang="ts">
import { inject } from "vue";
const theme = inject<Ref<"light" | "dark">>("theme");
</script>
<template>
<div :class="theme">{{ theme }}</div>
</template>InjectionKey で型安全
// types.ts
import type { InjectionKey, Ref } from "vue";
export const themeKey: InjectionKey<Ref<"light" | "dark">> = Symbol("theme");<!-- App.vue -->
<script setup lang="ts">
import { themeKey } from "./types.js";
const theme = ref<"light" | "dark">("light");
provide(themeKey, theme);
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { themeKey } from "./types.js";
const theme = inject(themeKey);
// theme: Ref<"light" | "dark"> | undefined
</script>→ key を InjectionKey で定義すると、inject 時に 完全な型推論。
デフォルト値
const theme = inject(themeKey, ref("light"));
// 祖先で provide されていなければ "light"undefined を返さないので使いやすい。
「state + 操作関数」を一緒に provide
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
const toggle = () => theme.value = theme.value === "light" ? "dark" : "light";
provide("theme", { theme, toggle });
</script><!-- Child.vue -->
<script setup lang="ts">
const { theme, toggle } = inject<{
theme: Ref<"light" | "dark">;
toggle: () => void;
}>("theme")!;
</script>
<template>
<button @click="toggle">{{ theme }}</button>
</template>→ React の Context value と同じパターン。
いつ使うか
- テーマ / ロケール (滅多に変わらない global)
- 認証状態
- ルーター内部
- フォームの context (FormProvider 的)
「頻繁更新の global state」は Pinia (L05 で詳しく) の方が向く。
React Context との対比
| React Context | Vue provide/inject | |
|---|---|---|
| 型安全 | TS 型付け | InjectionKey で完全 |
| 再描画範囲 | useContext した子全部 | 同様 (inject した先) |
| provider | <Provider value={...}> | provide("key", value) |
| 取得 | useContext(Ctx) | inject("key") |
まとめ
provide("key", value)+inject("key")で祖先 → 子孫InjectionKeyで 型安全 に- ref を provide すれば子からも更新可能 (リアクティブ維持)
- グローバル状態管理は Pinia (L05) を併用
L02 完。次は L03 ライフサイクル。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — Theme
問題
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
provide("theme", theme);
</script>
<template>
<Layout />
<button @click="theme = theme === 'light' ? 'dark' : 'light'">Toggle</button>
</template>
<!-- DeepChild.vue (Layout → Sidebar → Menu → ここ) -->
<script setup lang="ts">
import { inject, type Ref } from "vue";
const theme = inject<Ref<"light" | "dark">>("theme");
</script>
<template>
<div :class="theme">Theme: {{ theme }}</div>
</template>合格基準
- 中間 component を経由せず theme が取れる
- 親のボタンで子の表示が変わる
Lv2: 応用 — InjectionKey で型安全
問題
// keys.ts
import type { InjectionKey, Ref } from "vue";
export const themeKey: InjectionKey<Ref<"light" | "dark">> = Symbol("theme");<!-- App.vue -->
<script setup lang="ts">
import { themeKey } from "./keys.js";
const theme = ref<"light" | "dark">("light");
provide(themeKey, theme);
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { themeKey } from "./keys.js";
const theme = inject(themeKey);
// theme: Ref<"light" | "dark"> | undefined (型推論)
</script>合格基準
- InjectionKey で 型推論完全
- 文字列 key と比較して型が強くなることを確認
Lv3: 発展 — state + 操作関数を一緒に provide
問題
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
const toggle = () => theme.value = theme.value === "light" ? "dark" : "light";
provide("theme", { theme, toggle });
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
const { theme, toggle } = inject<{
theme: Ref<"light" | "dark">;
toggle: () => void;
}>("theme")!;
</script>
<template>
<button @click="toggle">{{ theme }}</button>
</template>子からも切替できる。
合格基準
- 子から toggle 呼べる
- React の Context value と同じパターン
提出
submission.md に3問の回答を。
ヒント
inject('theme', 'light')で祖先が provide した値を受け取る。第2引数は「祖先が誰も provide していないときの既定値」。- props と違い、直接の親でなくても(何階層離れていても)祖先が
provide('theme', ...)していれば受け取れる。 - 受け取った
themeをテンプレートのクラスに使う::class="\theme-${theme}\"。 - テストでは
global: { provide: { theme: 'dark' } }で祖先の provide を再現できる。
解説
provide / inject は「祖先から子孫へ直通」
props は親 → 子の 1 段ずつ。深い階層に値を届けるには、間のコンポーネント全部が
「自分は使わないのに props で受けて下に渡す」必要がある(prop drilling = バケツリレー):
App → Layout → Sidebar → Menu → ThemedButton ← theme を 4 段リレー
provide / inject は、このリレーを飛ばして祖先から子孫へ直接届ける:
// 祖先(App など)
provide('theme', 'dark')
// 子孫(何階層離れていても)
const theme = inject('theme', 'light') // 第2引数 = 既定値間の Layout / Sidebar / Menu は theme を一切知らなくてよい。
使いどころと注意
向いているもの:
- アプリ全体で共有する設定: テーマ、言語(i18n)、ログイン中ユーザー
- 「多くの子孫が使うが、間のコンポーネントには関係ない」値
注意:
- 使いすぎると「値の出どころ」が見えなくなる。
inject('theme')を見ても、どの祖先が
provide したか追いにくい。アプリ全体の横断関心(テーマ等)に限定し、通常の親子データは props/emit を使う。
- 大規模な状態共有は Pinia(
state-management等)の方が追跡しやすい。
テストでの再現
inject する子は、global.provide で祖先の provide を再現すればテストできる:
mount(Answer, { global: { provide: { theme: 'dark' } } })祖先コンポーネントを実際に組まなくても、inject の受け取りだけを検証できる。