404 Not Found

404 Not Found


nginx

Vue 3 の `provide` と `inject`:コンポーネント間のレベルを跨いだ通信

provideinject は、Vue 3 の レベル間通信 API です。親コンポーネントがデータを提供し、子コンポーネント(階層の深さに関係なく)はそれを注入して利用できます。これにより、「プロパティドリリング」という課題が解決されます。

Vue 3のprovideリアクティブデータ(ref / reactive)をサポートしており、子コンポーネントは注入されると自動的にリアクティブになります。このレッスンでは、この仕組みと5つの実用的なユースケースを習得できます。

1. 学習内容


2. 5つのレベルにわたって「テーマカラー」をプロップドリルで掘り進める悪夢

(1) 課題:アプリ → レイアウト → ヘッダー → ユーザーメニュー → ボタン、5層のパススルー

アリスの管理ダッシュボードでは、テーマの切り替えに対応する必要がありました:

VUE
<!-- ❌ The "Flip" Version: 5-Layer prop drilling -->
<!-- App.vue -->
<Layout :theme="theme" :user="user" :locale="locale">
  <router-view />
</Layout>

<!-- Layout.vue -->
<Header :theme="theme" :user="user" :locale="locale" />

<!-- Header.vue -->
<UserMenu :theme="theme" :user="user" :locale="locale" />

<!-- UserMenu.vue -->
<ThemeButton :theme="theme" />
<LocaleSelector :locale="locale" />

プロダクトマネージャーのチャーリーが、さらに3つの設定を追加しました:

「アリス、サイドバーの折りたたみ設定、通知設定、機能フラグの設定を追加する必要がある。5つのコンポーネントに8つのプロパティを渡すことになる。これではメンテナンスが不可能だ。」

(2) Vueによる提供/注入の仕組み:親要素に注入し、子要素が受け取る

VUE
<!-- App.vue Ancestors -->
<script setup>
import { ref, provide } from 'vue'

const theme = ref('light')
const user = ref({ name: 'Alice' })
const locale = ref('zh-CN')

// ✅ Once provide,All descendants may inject
provide('theme', theme)
provide('user', user)
provide('locale', locale)
</script>

<!-- UserMenu.vue Any descendant at any depth -->
<script setup>
import { inject } from 'vue'

// ✅ Get it directly, No props passthrough needed
const theme = inject('theme')
const user = inject('user')
const locale = inject('locale')
</script>

<template>
  <button :class="theme">Toggle {{ user.name }}</button>
  <p>{{ locale }}</p>
</template>

5段階のネストprovide 1行 + inject 1行

(3) 収益

提供/注入後:


3. provide および inject の基本的な構文

(1) 入力データを指定する

VUE
<!-- App.vue Ancestor Components -->
<script setup>
import { ref, provide } from 'vue'

const theme = ref('light')
const user = { name: 'Alice' }

// 1. Inject a static value
provide('appName', 'My Admin')

// 2. Injecting Responsive Data(ref)
provide('theme', theme)

// 3. Injecting Responsive Data(reactive)
provide('user', user)

// 4. Injection Methods
provide('updateUser', (newUser) => {
  user.value = newUser
})
</script>

(2) injectがデータを受信する

VUE
<!-- Child.vue Any descendant component -->
<script setup>
import { inject } from 'vue'

// 1. Basic Reception
const theme = inject('theme')

// 2. With default values(My ancestors are no longer with us provide When to Use)
const theme = inject('theme', 'light')

// 3. Include factory function defaults
const theme = inject('theme', () => 'light')

// 4. Type Assertion(TypeScript)
const theme = inject<string>('theme', 'light')
</script>

(3) 完全な例

VUE
<!-- Ancestor.vue -->
<script setup>
import { ref, provide } from 'vue'

const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })

provide('count', count)        // ref
provide('user', user)          // reactive
provide('config', { theme: 'dark' })  // Object
provide('reset', () => { count.value = 0 })  // Methods
</script>

<!-- DeepChild.vue(Any depth) -->
<script setup>
import { inject } from 'vue'

const count = inject('count')        // Responsive
const user = inject('user')          // Responsive
const config = inject('config')      // Ordinary Object
const reset = inject('reset')        // Function

// Edit
function increment() {
  count.value++  // ✅ Ancestor components will also respond
}
</script>

4. シンボル型をインジェクトキーとして使用する場合

(1) なぜSymbolを使うのか?

文字列キーは競合が生じやすい(同じキーを持つ複数の provide ステートメントがあると、互いに上書きされてしまう)。シンボルは一意であるため、競合を防ぐことができる。

VUE
<!-- keys.js - Centrally manage all provide key -->
<script>
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CONFIG_KEY = Symbol('config')
</script>
VUE
<!-- Ancestor.vue Ancestor Components -->
<script setup>
import { provide, ref } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY } from './keys'

provide(THEME_KEY, ref('light'))
provide(USER_KEY, ref({ name: 'Alice' }))
provide(LOCALE_KEY, ref('zh-CN'))
</script>
VUE
<!-- DeepChild.vue Any descendants -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './keys'

// ✅ Type Safety:Must use Symbol talent inject
const theme = inject(THEME_KEY)
const user = inject(USER_KEY)
</script>

(2) 5つの主な利点

利点 説明
一意性 シンボルはグローバルに一意であり、競合は発生しない
型安全性 TypeScript は正確に型を推論できる
一元管理 すべての鍵は単一のファイルに保存される
リファクタリングに適している キーを変更しても他のキーには影響しない
可読性 キー名を見るだけで、その用途がわかる

5. レスポンシブ provide の詳細な解説

(1) 「provide」の3つのモード

VUE
<!-- Ancestor.vue -->
<script setup>
import { ref, reactive, provide } from 'vue'

// 1. Inject ref(Responsive)
const count = ref(0)
provide('count', count)

// 2. Inject reactive(Responsive)
const user = reactive({ name: 'Alice' })
provide('user', user)

// 3. Injecting Read-Only Objects(Non-responsive)
const config = { theme: 'dark' }
provide('config', config)
</script>

(2) 変更を行う2つの方法

VUE
<!-- Method 1:Directly Modify Ancestors(Recommendations) -->
<!-- Ancestor.vue -->
<script setup>
import { ref, provide } from 'vue'

const theme = ref('light')
provide('theme', theme)

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

<!-- Method 2:Through their descendants inject Edit Citation -->
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')

function toggleTheme() {
  theme.value = 'dark'  // ✅ The change is ref,Both the ancestors and other descendants responded
}
</script>

(3) 5つの主要な対応シナリオ

シナリオ 用途
テーマを切り替える ref + provide
ユーザーログイン reactive + provide
i18n ロケール 参照 + 提供 + 切り替え方法
グローバル読み込み ref(false) + 提供
グローバルメッセージ通知 リアクティブ + 提供

6. 実社会における5つの主要なシナリオ

(1) シーン1:話題の切り替え

JS
// keys.js
export const THEME_KEY = Symbol('theme')
VUE
<!-- App.vue Ancestors -->
<script setup>
import { ref, provide } from 'vue'
import { THEME_KEY } from './keys'

const theme = ref('light')
provide(THEME_KEY, theme)
</script>

<!-- ThemeButton.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './keys'

const theme = inject(THEME_KEY)
</script>

<template>
  <button @click="theme.value = theme === 'light' ? 'dark' : 'light'">
    {{ theme === 'light' ? '🌞' : '🌙' }}
  </button>
</template>

(2) シナリオ 2:ユーザー認証

JS
// keys.js
export const AUTH_KEY = Symbol('auth')
VUE
<!-- App.vue -->
<script setup>
import { reactive, provide } from 'vue'
import { AUTH_KEY } from './keys'

const auth = reactive({
  user: null,
  isLoggedIn: false,
  login(credentials) { /* API call */ },
  logout() { this.user = null; this.isLoggedIn = false }
})
provide(AUTH_KEY, auth)
</script>

<!-- UserMenu.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { AUTH_KEY } from './keys'

const auth = inject(AUTH_KEY)
</script>

<template>
  <div v-if="auth.isLoggedIn">{{ auth.user.name }}</div>
  <button v-else @click="auth.login(creds)">Login</button>
</template>

(3) シナリオ 3: i18n

JS
// keys.js
export const I18N_KEY = Symbol('i18n')
VUE
<!-- App.vue -->
<script setup>
import { ref, provide, computed } from 'vue'
import { I18N_KEY } from './keys'

const messages = {
  'zh-CN': { hello: 'Hello', welcome: 'Welcome' },
  'en-US': { hello: 'Hello', welcome: 'Welcome' }
}

const locale = ref('zh-CN')
const t = computed(() => (key) => messages[locale.value][key])

provide(I18N_KEY, { locale, t })
</script>

<!-- Hello.vue Descendants -->
<script setup>
import { inject } from 'vue'
import { I18N_KEY } from './keys'

const { t } = inject(I18N_KEY)
</script>

<template>
  <h1>{{ t('hello') }}, {{ t('welcome') }}</h1>
</template>

(4) シナリオ4:グローバルロード

VUE
<!-- App.vue -->
<script setup>
import { ref, provide } from 'vue'

const loading = ref(false)
provide('loading', loading)
</script>

<!-- AnyComponent.vue Descendants -->
<script setup>
import { inject } from 'vue'
const loading = inject('loading')

async function fetchData() {
  loading.value = true
  await fetch('/api/data')
  loading.value = false
}
</script>

<template>
  <button @click="fetchData">Refresh</button>
</template>

(5) シナリオ 5:テーマ + ビジネスデータ

VUE
<!-- App.vue -->
<script setup>
import { ref, reactive, provide } from 'vue'

const appState = reactive({
  theme: 'light',
  user: { name: 'Alice' },
  permissions: ['read', 'write'],
  config: { sidebar: true }
})
provide('appState', appState)
</script>

7. provide / inject とその他のアプローチの比較

(1) 5つのコミュニケーション手法の比較

側面 プロパティ/エミット 提供/注入 イベントバス Pinia mitt
父と息子の文通 ⭐⭐⭐⭐⭐ - - - -
レベル横断 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Brothers Communications ❌(保護者の仲介が必要) ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
レスポンシブ ❌ (手動)
型安全性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
DevTools ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
習得の難易度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐

(2) 5つの主要な選定シナリオ

シナリオ 推奨される解決策
親子間のコミュニケーション props/emit
レベルをまたぐグローバル状態(テーマ/認証/i18n) 提供/注入
中規模~大規模アプリケーションにおけるグローバル状態 Pinia (フェーズ 4.2)
シンプルなイベントバス mitt / tiny-emitter
複雑なビジネス状態の管理 Pinia + Vue Router

8. 完全な例:ECサイトのバックエンドでのテーマの切り替え

(1) ▶ サンプル:. 鍵の一元管理

JS
// src/keys.js
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CART_KEY = Symbol('cart')
▶ 試してみよう

(2) ▶ サンプル:. App.vue—1回限り provide

VUE
<!-- src/App.vue -->
<script setup>
import { ref, reactive, provide, readonly } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY, CART_KEY } from './keys'

// Topic
const theme = ref('light')
function setTheme(newTheme) {
  theme.value = newTheme
  localStorage.setItem('theme', newTheme)
}

// User
const user = reactive({
  id: 1,
  name: 'Alice',
  role: 'admin'
})

// Internationalization
const locale = ref('zh-CN')
const messages = {
  'zh-CN': { home: 'Home', cart: 'Shopping Cart' },
  'en-US': { home: 'Home', cart: 'Cart' }
}
const t = (key) => messages[locale.value][key]

// Shopping Cart
const cart = reactive({ items: [], total: 0 })

// Disposable provide 4 Global state
provide(THEME_KEY, { theme, setTheme })
provide(USER_KEY, { user })
provide(LOCALE_KEY, { locale, t })
provide(CART_KEY, { cart })
</script>

<template>
  <router-view />
</template>
▶ 試してみよう

(3) ▶ サンプル:. 子孫による使用(5つの使い方)

VUE
<!-- DeepChild.vue Any depth -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY, USER_KEY, LOCALE_KEY, CART_KEY } from './keys'

// 1. Deconstruction
const { theme, setTheme } = inject(THEME_KEY)
const { user } = inject(USER_KEY)
const { locale, t } = inject(LOCALE_KEY)
const { cart } = inject(CART_KEY)
</script>

<template>
  <div :class="['app', theme]">
    <p>{{ user.name }} ({{ user.role }})</p>
    <p>{{ t('home') }} | {{ t('cart') }}</p>
    <p>{{ cart.items.length }} items, ${{ cart.total }}</p>
    
    <button @click="setTheme('dark')">Dark Mode</button>
  </div>
</template>
▶ 試してみよう

(4) ▶ サンプル:. シンボルインジェクションキーの5つの主な利点の実演

JS
// src/keys.js
export const THEME_KEY = Symbol('theme')  // The Only One
export const ANOTHER_THEME_KEY = Symbol('theme')  // Also the only one(Different Symbol)
▶ 試してみよう
VUE
<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
import { THEME_KEY, ANOTHER_THEME_KEY } from './keys'

const theme1 = ref('light')
const theme2 = ref('dark')

// ✅ There will be no conflict:Different Symbol Even if the names are the same
provide(THEME_KEY, theme1)
provide(ANOTHER_THEME_KEY, theme2)
</script>

(5) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス

エラー 症状 解決策
文字列キーの競合 上書き シンボルを使用
祖先以外の提供 未定義 コンポーネント階層を確認
読み取り専用プロパティの変更 警告 親オブジェクトを変更しました。子オブジェクトは読み取り専用です
デフォルト値を無視 未定義 inject(key, defaultValue)
provide ステートメントが多数ある 保守が困難 keys.js に集中している

(6) ▶ サンプル:. 5つの主要なパフォーマンス比較

モード 性能 適用対象
provide('key', value) ⭐⭐⭐⭐⭐ 固定値
provide('key', ref) ⭐⭐⭐⭐⭐ レスポンシブ評価
provide('key', reactive) ⭐⭐⭐⭐ 複雑なオブジェクト
provide('key', computed) ⭐⭐⭐⭐⭐ 算出値
provide('key', function) ⭐⭐⭐⭐ 使い方

❓ よくある質問

Q provideprops、どちらを使うべきですか?
A 親コンポーネントと子コンポーネント間の直接通信(明示的かつ型安全)には props を使用してください。プロパティドリリングを回避するために、レベルをまたぐ通信(3レベル以上)には provide を使用してください。大規模なアプリケーションにおけるグローバル状態の管理には、Pinia(Phase 4.2)を使用してください。
Q 提供されるデータはリアクティブですか?
A 提供されるデータによって異なります。provide('key', ref) リアクティブ、provide('key', {a: 1}) 非リアクティブ(通常のオブジェクト)。provide ref または provide reactive の使用をお勧めします。
Q providesetup の最上位レベルで呼び出す必要がありますか?
A はい。provide<script setup> の最上位レベルで呼び出す必要があります(ライフサイクルフック内での呼び出しは許可されていません)。そうしないと、provide は有効になりません。
Q inject の後に provide の値を変更することはできますか?
A ref.value を変更することは可能ですが、親コンポーネント内でのみ(provide メソッドを使用して)変更することをお勧めします。ref を直接変更すると、データの流れが不明確になる可能性があります。
Q シンボルキーはどこで定義されていますか?
A すべてのシンボルをエクスポートする別のファイル src/keys.js です。使用されるすべてのコンポーネントは、このファイルからインポートされます。
Q provide/inject と Pinia のどちらを選べばよいですか?
A 小規模なプロジェクト(グローバル状態が 5 つ未満)の場合は、provide/inject で十分です。中規模から大規模なプロジェクト(グローバル状態が 10 以上)の場合は、Pinia の方が DevTools への対応が充実しているため、Pinia を使用してください。
Q provide を含むコンポーネントがアンマウントされるとどうなりますか?
A すべての子孫コンポーネントもアンマウントされます(Vue コンポーネントツリーのライフサイクル)。inject は再び上方向の祖先を検索します。

📖 まとめ


📝 練習問題

(1) 基礎問題(難易度:⭐)

簡単なテーマ切り替え機能を実装する:

(2) 上級問題(難易度:⭐⭐)

ユーザー認証を実装する(提供/注入):

(3) チャレンジ問題(難易度:⭐⭐⭐)

eコマースのバックエンド向けに、包括的なグローバル状態管理システムを実装する:

  1. 4つのキー:THEME / USER / CART / LOCALE
  2. App.vue は 4 つのグローバル状態を一度に提供します
  3. 5つのコンポーネントを異なるレベル(ヘッダー/サイドバー/商品一覧/カート/フッター)で使用すること
  4. コンポーネント間の連携:ヘッダーでログアウトがトリガーされると、サイドバーが自動的に更新される
  5. テーマの切り替え + 国際化(i18n)の切り替え + ショッピングカートへの商品の追加・削除
  6. 強型付けのためのTypeScriptの使用
  7. 5 パターンの提供(Value/ref/reactive/computed/function)
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%