採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

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 ContextVue 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 の受け取りだけを検証できる。