0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3 × TypeScriptで理解するProvide / InjectとInjectionKey

0
Last updated at Posted at 2026-07-30

Vue 3 × TypeScriptで理解するProvide / InjectとInjectionKey

Vue.jsで複数階層のコンポーネント間にデータを渡す場合、基本的にはPropsを使用します。

しかし、親コンポーネントから孫・ひ孫コンポーネントへデータを渡す場合、途中のコンポーネントでもPropsを受け取り、さらに下のコンポーネントへ渡す必要があります。

このようなPropsの受け渡しが続く状態は、一般的にProps drillingと呼ばれます。

Vue 3では、Provide / Injectを利用することで、途中のコンポーネントを経由せずに、子孫コンポーネントへデータや関数を共有できます。

本記事では、以下の内容について解説します。

  • Provide / Injectの基本
  • TypeScriptでの型指定
  • InjectionKeyの使い方
  • 値をリアクティブに共有する方法
  • 実際のコンポーネント構成例
  • 利用時の注意点

Provide / Injectとは

Provide / Injectは、祖先コンポーネントから子孫コンポーネントへ値を共有するための仕組みです。

値を提供する側でprovide()を使用し、値を受け取る側でinject()を使用します。

App.vue
└── ParentComponent.vue
    └── ChildComponent.vue
        └── GrandChildComponent.vue

例えば、App.vueで提供した値は、GrandChildComponent.vueから直接取得できます。

途中にあるParentComponent.vueChildComponent.vueでPropsを受け渡す必要はありません。


Provideの基本

値を提供する側では、Vueからprovideをインポートします。

<script setup lang="ts">
import { provide } from 'vue'

provide('message', 'Hello Provide / Inject')
</script>

<template>
  <div>
    <h1>親コンポーネント</h1>
  </div>
</template>

provide()の第1引数にはキー、第2引数には共有したい値を指定します。

provide('message', 'Hello Provide / Inject')

この例では、messageというキーで文字列を提供しています。


Injectの基本

値を受け取る側では、injectを使用します。

<script setup lang="ts">
import { inject } from 'vue'

const message = inject('message')
</script>

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

inject()に、provide()で指定したものと同じキーを渡します。

const message = inject('message')

これにより、祖先コンポーネントが提供した値を取得できます。


TypeScript利用時の問題点

文字列をキーとしてinject()を使用した場合、取得結果にはundefinedの可能性があります。

const message = inject<string>('message')

この場合、messageの型は次のようになります。

string | undefined

なぜなら、対応するprovide()が存在しない可能性があるためです。

そのため、テンプレートや処理内で使用する際には、undefinedを考慮する必要があります。

if (message) {
  console.log(message)
}

または、デフォルト値を指定できます。

const message = inject<string>('message', 'デフォルトメッセージ')

デフォルト値を指定すると、messagestringとして扱えます。


文字列キーを使う場合の問題

次のように文字列をキーとして使用することもできます。

provide('user', {
  id: 1,
  name: '山田太郎'
})
const user = inject<User>('user')

ただし、文字列キーにはいくつか問題があります。

  • キー名の入力ミスが起こる可能性がある
  • 別の場所で同じキー名が使われる可能性がある
  • provide側とinject側の型が自動的に関連付けられない
  • 大規模なアプリケーションでは管理しにくい

例えば、次のようにキー名を間違えても、TypeScriptは検知できません。

provide('user', user)
const user = inject<User>('users')

userusersでキーが異なるため、値を取得できません。

この問題を避けるために使用できるのが、InjectionKeyです。


InjectionKeyとは

InjectionKeyは、Provide / Injectで使用するキーに型情報を持たせるための仕組みです。

InjectionKeyを使用すると、provide()で渡す値とinject()で受け取る値の型を統一できます。

内部的にはSymbolをキーとして使用します。

import type { InjectionKey } from 'vue'

InjectionKeyの定義

まず、共有するデータの型を定義します。

export type User = {
  id: number
  name: string
}

次に、InjectionKeyを定義します。

import type { InjectionKey } from 'vue'
import type { User } from './types'

export const userKey: InjectionKey<User> = Symbol('user')

InjectionKey<User>と指定することで、このキーに対応する値はUser型であることを表現できます。


ファイル構成例

キーや型を専用ファイルへ分けておくと管理しやすくなります。

src
├── components
│   ├── ParentComponent.vue
│   └── UserProfile.vue
├── keys
│   └── userKey.ts
├── types
│   └── user.ts
└── App.vue

型定義ファイル

src/types/user.ts

export type User = {
  id: number
  name: string
  email: string
}

InjectionKeyの定義ファイル

src/keys/userKey.ts

import type { InjectionKey } from 'vue'
import type { User } from '../types/user'

export const userKey: InjectionKey<User> = Symbol('user')

Symbol()を使うことで、同じ名前のキーが別の場所で作成されても衝突しません。

Symbol('user') === Symbol('user')

上記の結果はfalseになります。

それぞれ別のSymbolとして扱われるためです。


Provideする側

App.vue

<script setup lang="ts">
import { provide } from 'vue'
import { userKey } from './keys/userKey'
import type { User } from './types/user'
import UserProfile from './components/UserProfile.vue'

const user: User = {
  id: 1,
  name: '山田太郎',
  email: 'yamada@example.com'
}

provide(userKey, user)
</script>

<template>
  <main>
    <h1>ユーザー情報</h1>
    <UserProfile />
  </main>
</template>

userKeyInjectionKey<User>として定義されているため、異なる型の値を渡すとTypeScriptのエラーになります。

provide(userKey, '山田太郎')

この場合、userKeyUser型を要求しているため、文字列を渡すことはできません。


Injectする側

UserProfile.vue

<script setup lang="ts">
import { inject } from 'vue'
import { userKey } from '../keys/userKey'

const user = inject(userKey)
</script>

<template>
  <section v-if="user">
    <p>ID:{{ user.id }}</p>
    <p>名前:{{ user.name }}</p>
    <p>メールアドレス:{{ user.email }}</p>
  </section>

  <p v-else>
    ユーザー情報を取得できませんでした。
  </p>
</template>

userKeyに型情報が含まれているため、次のように型引数を指定する必要はありません。

const user = inject(userKey)

userの型は自動的に次のようになります。

User | undefined

リアクティブな値を共有する

通常のオブジェクトをprovide()した場合、値を更新してもリアクティブな変更として扱われないケースがあります。

リアクティブな状態を共有したい場合は、ref()reactive()を使用します。


Refを共有する例

まず、共有する型を定義します。

import type { InjectionKey, Ref } from 'vue'

export const countKey: InjectionKey<Ref<number>> = Symbol('count')

提供する側では、ref()で値を定義します。

<script setup lang="ts">
import { provide, ref } from 'vue'
import { countKey } from './keys/countKey'
import CounterDisplay from './components/CounterDisplay.vue'

const count = ref(0)

provide(countKey, count)
</script>

<template>
  <div>
    <button @click="count++">
      親側でカウントアップ
    </button>

    <CounterDisplay />
  </div>
</template>

受け取る側では、同じcountKeyを使用します。

<script setup lang="ts">
import { inject } from 'vue'
import { countKey } from '../keys/countKey'

const count = inject(countKey)
</script>

<template>
  <p v-if="count">
    カウント:{{ count }}
  </p>
</template>

テンプレート内では、ref.valueは自動的に展開されます。

そのため、次のように記述できます。

{{ count }}

JavaScriptやTypeScriptの処理内では、.valueが必要です。

if (count) {
  console.log(count.value)
}

状態と更新処理をまとめて共有する

状態だけでなく、更新用の関数もProvide / Injectで共有できます。

例えば、Todoリストの状態と追加処理を共有する場合を考えます。


Todoの型定義

src/types/todo.ts

export type Todo = {
  id: number
  title: string
  completed: boolean
}

Contextの型を定義する

import type { Ref } from 'vue'
import type { Todo } from './todo'

export type TodoContext = {
  todos: Ref<Todo[]>
  addTodo: (title: string) => void
  toggleTodo: (id: number) => void
}

InjectionKeyを定義する

src/keys/todoKey.ts

import type { InjectionKey } from 'vue'
import type { TodoContext } from '../types/todoContext'

export const todoKey: InjectionKey<TodoContext> = Symbol('todo')

Provideする側

<script setup lang="ts">
import { provide, ref } from 'vue'
import TodoList from './components/TodoList.vue'
import { todoKey } from './keys/todoKey'
import type { Todo } from './types/todo'

const todos = ref<Todo[]>([
  {
    id: 1,
    title: 'Vue.jsを学習する',
    completed: false
  }
])

const addTodo = (title: string): void => {
  const trimmedTitle = title.trim()

  if (!trimmedTitle) {
    return
  }

  todos.value.push({
    id: Date.now(),
    title: trimmedTitle,
    completed: false
  })
}

const toggleTodo = (id: number): void => {
  const todo = todos.value.find((item) => item.id === id)

  if (!todo) {
    return
  }

  todo.completed = !todo.completed
}

provide(todoKey, {
  todos,
  addTodo,
  toggleTodo
})
</script>

<template>
  <TodoList />
</template>

Injectする側

<script setup lang="ts">
import { inject, ref } from 'vue'
import { todoKey } from '../keys/todoKey'

const todoContext = inject(todoKey)
const inputTitle = ref('')

const handleAddTodo = (): void => {
  if (!todoContext) {
    return
  }

  todoContext.addTodo(inputTitle.value)
  inputTitle.value = ''
}
</script>

<template>
  <section v-if="todoContext">
    <div>
      <input
        v-model="inputTitle"
        type="text"
        placeholder="Todoを入力"
      >

      <button @click="handleAddTodo">
        追加
      </button>
    </div>

    <ul>
      <li
        v-for="todo in todoContext.todos.value"
        :key="todo.id"
      >
        <label>
          <input
            type="checkbox"
            :checked="todo.completed"
            @change="todoContext.toggleTodo(todo.id)"
          >

          <span :class="{ completed: todo.completed }">
            {{ todo.title }}
          </span>
        </label>
      </li>
    </ul>
  </section>

  <p v-else>
    Todo情報を取得できませんでした。
  </p>
</template>

<style scoped>
.completed {
  text-decoration: line-through;
}
</style>

このように、データと操作用の関数をまとめて共有できます。


Injectした値のundefinedを処理する方法

inject()の戻り値には、基本的にundefinedの可能性があります。

対応方法はいくつかあります。

v-ifで確認する

<template>
  <div v-if="user">
    {{ user.name }}
  </div>
</template>

最も安全で分かりやすい方法です。


デフォルト値を指定する

const user = inject(userKey, {
  id: 0,
  name: 'ゲスト',
  email: ''
})

Providerが存在しない場合は、指定したデフォルト値が使用されます。


エラーを発生させる

Providerが必須である場合は、値が存在しない時点でエラーを発生させる方法もあります。

const user = inject(userKey)

if (!user) {
  throw new Error('userKeyがprovideされていません')
}

この処理以降では、userUser型として扱われます。


共通関数を作成する

同じチェックを複数のコンポーネントで行う場合は、Composableとして切り出すと便利です。

import { inject } from 'vue'
import { userKey } from '../keys/userKey'

export const useUser = () => {
  const user = inject(userKey)

  if (!user) {
    throw new Error('useUserはuserKeyのProvider配下で使用してください')
  }

  return user
}

コンポーネント側では、次のように使用できます。

<script setup lang="ts">
import { useUser } from '../composables/useUser'

const user = useUser()
</script>

<template>
  <p>{{ user.name }}</p>
</template>

この方法では、各コンポーネントで毎回undefinedを確認する必要がありません。


readonlyを使用する

Inject側から状態を自由に変更させたくない場合は、readonly()を使用できます。

import { provide, readonly, ref } from 'vue'

const count = ref(0)

const increment = (): void => {
  count.value++
}

provide(countKey, {
  count: readonly(count),
  increment
})

状態の更新は、提供側が用意した関数を通して行います。

context.increment()

この構成にすると、状態変更の責任をProvider側に集約できます。

context.count.value++

のようにInject側から直接変更することを防ぎやすくなります。


PropsとProvide / Injectの使い分け

PropsとProvide / Injectには、それぞれ適した用途があります。

Propsが適しているケース

  • 親子間でデータを渡す
  • コンポーネントの依存関係を明確にしたい
  • 再利用可能なコンポーネントを作りたい
  • データの流れを追いやすくしたい
<UserProfile :user="user" />

親子間の単純な受け渡しであれば、基本的にはPropsを使用します。


Provide / Injectが適しているケース

  • 複数階層のコンポーネントへデータを渡したい
  • テーマや認証情報などを子孫全体で共有したい
  • Props drillingを避けたい
  • 特定のコンポーネントツリー内で状態を共有したい

具体例としては、以下のような用途があります。

  • ログインユーザー情報
  • テーマ設定
  • 言語設定
  • フォーム全体の状態
  • タブやアコーディオンの内部状態
  • 共通の操作関数

Piniaとの違い

Provide / InjectとPiniaは、どちらも状態共有に利用できますが、用途が異なります。

Provide / Inject

特定のコンポーネントツリー内で状態を共有する場合に向いています。

FormProvider
└── InputGroup
    └── CustomInput

このフォーム内だけで必要な状態であれば、Provide / Injectが適しています。

Pinia

アプリケーション全体で利用するグローバルな状態管理に向いています。

App全体
├── Header
├── Main
└── MyPage

ログインユーザー情報やショッピングカートなど、複数の画面から利用する状態はPiniaの方が管理しやすい場合があります。

Provide / Injectですべての状態を管理しようとすると、どこから値が提供されているか分かりにくくなるため、用途に応じた使い分けが必要です。


Provide / Injectを使う際の注意点

使いすぎるとデータの流れが分かりにくくなる

Propsはテンプレートを見るだけで、どの値が渡されているか確認できます。

<UserProfile :user="user" />

一方、Injectは祖先コンポーネントから暗黙的に値を受け取ります。

const user = inject(userKey)

どこでProvideされているかを探す必要があるため、使いすぎると依存関係が分かりにくくなります。


キーは共通ファイルで管理する

Provide側とInject側で別々にSymbolを作成してはいけません。

provide(Symbol('user'), user)
const user = inject(Symbol('user'))

同じuserという説明を持っていても、異なるSymbolとして扱われます。

必ず同じキーをインポートして使用します。

import { userKey } from './keys/userKey'

状態の変更箇所を限定する

Inject側から直接状態を更新できる設計にすると、変更箇所が分散します。

そのため、更新用関数をProvide側に定義する方法が安全です。

provide(todoKey, {
  todos: readonly(todos),
  addTodo,
  toggleTodo
})

まとめ

Vue 3のProvide / Injectを使用すると、祖先コンポーネントから子孫コンポーネントへ、途中のコンポーネントを経由せずに値を共有できます。

特にTypeScriptを使用する場合は、文字列キーではなくInjectionKeyを利用することで、型安全に実装できます。

export const userKey: InjectionKey<User> = Symbol('user')

Provide側では、キーに対応する型の値を提供します。

provide(userKey, user)

Inject側では、キーから型が推論されます。

const user = inject(userKey)

重要なポイントは次のとおりです。

  • 単純な親子間のデータ受け渡しにはPropsを使用する
  • 複数階層で値を共有する場合はProvide / Injectを検討する
  • TypeScriptではInjectionKeyを使用する
  • リアクティブな値にはrefreactiveを使用する
  • 状態更新用の関数もまとめてProvideできる
  • Inject側でundefinedを考慮する
  • 状態の直接変更を防ぐ場合はreadonlyを使用する
  • アプリ全体の状態管理にはPiniaも検討する

Provide / Injectは便利な仕組みですが、すべてのデータ共有に使用するのではなく、PropsやPiniaと役割を分けることが重要です。

補足:Symbol()Symbol.for()の違い

Vue 3のProvide / Injectでは、InjectionKeyとしてSymbol()を使用することが一般的です。

export const userKey: InjectionKey<User> = Symbol("user-key");

通常はこの書き方で問題ありません。


Symbol()の特徴

Symbol()は呼び出すたびに新しい一意のSymbolを生成します。

const key1 = Symbol("user");
const key2 = Symbol("user");

console.log(key1 === key2); // false

同じ説明文字列を指定しても、別のSymbolになります。


Symbol.for()の特徴

Symbol.for()グローバルなSymbolレジストリを利用します。

同じキー名で呼び出すと、常に同じSymbolが返されます。

const key1 = Symbol.for("user");
const key2 = Symbol.for("user");

console.log(key1 === key2); // true

開発時はSymbol.for()が役立つ場合がある

Viteなどでは**HMR(Hot Module Replacement)**によって、ページ全体をリロードせず、変更したモジュールだけを再読み込みできます。

このとき、

export const userKey = Symbol("user-key");

としていると、モジュールの再読み込み時に新しいSymbolが生成される可能性があります。

その結果、provide()inject()で異なるSymbolを参照してしまい、値を取得できないケースがあります。

そのような場合は、

export const userKey = Symbol.for("user-key");

としておくことで、同じキー名から同じSymbolを取得できるため、HMR時のトラブルを避けやすくなります。


どちらを使うべき?

利用シーン おすすめ
一般的なVueアプリ Symbol()
HMRを利用した開発環境 Symbol.for()も検討
ライブラリ間でキーを共有する Symbol.for()
モジュールをまたいで同じSymbolを使いたい Symbol.for()

まとめ

Symbol()は毎回新しいSymbolを生成するため、一意性を保証したい場合に適しています。一方、Symbol.for()は同じ名前から常に同じSymbolを取得できるため、HMR環境や複数モジュールでキーを共有するケースで役立ちます。

普段のVueアプリ開発ではSymbol()で十分なケースがほとんどですが、開発環境やライブラリ開発ではSymbol.for()を選択することで、より安定した動作につながる場合があります。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?