本文 AI 產出,尚未審核

Vue3 Composition API(核心) – provide / inject

簡介

在大型 Vue3 應用中,元件之間的資料傳遞往往不只是一層或兩層的父子關係。若僅靠 props / emit,會讓中間層元件變成「傳遞屬性」(prop‑drilling) 的搬運工,增加維護成本。
Composition API 為我們提供了 provideinject 這對 API,讓「上層」元件可以直接將資料或函式「提供」給「任意深度」的子孫元件,而不需要在每一層都顯式傳遞。這不僅讓程式碼更乾淨,也讓功能模組化、可重用性提升。

本篇將從概念說明、實作範例、常見陷阱與最佳實踐,最後以實務場景說明,幫助你在 Vue3 中熟練使用 provide / inject


核心概念

1. 為什麼需要 provide / inject

  • 跨層級共享:在深層子元件需要存取父層狀態或服務時,避免層層傳遞。
  • 依賴注入 (DI):可以把「服務」(如 API client、全域狀態、表單驗證器) 注入到需要的元件,保持元件的低耦合。
  • 組件封裝:父元件只需要提供介面,子元件只需要 inject,不必知道父元件的實作細節。

注意provide / inject 並不是全域狀態管理的替代方案(如 Pinia),它的作用範圍僅限於「祖先‑子孫」樹狀結構,且不具備響應式的自動追蹤(除非自行處理)。

2. 基本使用方式(Composition API)

方法 位置 語法
provide 父元件的 setup provide(key, value)
inject 子孫元件的 setup inject(key, defaultValue?)
  • key 可以是字串或 Symbol,建議使用 Symbol 以避免衝突。
  • value 若是 refreactivecomputed,子元件取得的仍是同一個響應式物件,變更會自動同步。

3. 透過 Symbol 定義依賴

// src/injectionKeys.js
export const ThemeSymbol = Symbol('theme')
export const AuthServiceSymbol = Symbol('authService')

使用 Symbol 可以確保「提供」與「注入」的鍵名唯一,避免在大型專案中不小心覆寫。

4. 提供響應式資料

// ParentComponent.vue
<script setup>
import { provide, ref } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

const theme = ref('light')           // 這是一個響應式的資料
provide(ThemeSymbol, theme)         // 把 ref 本身提供出去
</script>

<template>
  <slot />   <!-- 子孫元件會自動取得 theme -->
</template>
// ChildComponent.vue
<script setup>
import { inject } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

const theme = inject(ThemeSymbol)    // 直接取得同一個 ref
</script>

<template>
  <div :class="`theme-${theme}`">Current theme: {{ theme }}</div>
  <button @click="theme = theme === 'light' ? 'dark' : 'light'">
    切換主題
  </button>
</template>

重點provide 時傳入的是 ref 本身,而非 ref.value,這樣子元件才會取得同一個響應式引用,變更會同步。

5. 提供函式或服務

// AuthProvider.vue
<script setup>
import { provide } from 'vue'
import { AuthServiceSymbol } from '@/injectionKeys'

class AuthService {
  login(username, password) {
    // 假設這裡呼叫 API
    console.log(`Logging in ${username}`)
    return Promise.resolve({ token: 'abc123' })
  }
  logout() {
    console.log('Logging out')
  }
}

const authService = new AuthService()
provide(AuthServiceSymbol, authService)   // 提供實例
</script>

<template>
  <slot />
</template>
// LoginForm.vue
<script setup>
import { inject } from 'vue'
import { AuthServiceSymbol } from '@/injectionKeys'

const auth = inject(AuthServiceSymbol)   // 取得服務實例
const username = ref('')
const password = ref('')

const handleLogin = async () => {
  const result = await auth.login(username.value, password.value)
  console.log('Login result:', result)
}
</script>

<template>
  <form @submit.prevent="handleLogin">
    <input v-model="username" placeholder="帳號" />
    <input v-model="password" type="password" placeholder="密碼" />
    <button type="submit">登入</button>
  </form>
</template>

6. 預設值與錯誤處理

// DeepChild.vue
<script setup>
import { inject } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

// 若上層未提供,使用預設值
const theme = inject(ThemeSymbol, ref('default'))

if (!theme) {
  console.warn('Theme not provided, using default.')
}
</script>

<template>
  <p>使用的主題是:{{ theme }}</p>
</template>
  • 第二個參數可以是 預設值(任意型別),如果上層沒有 provide,Vue 會直接回傳這個值。
  • 若預設值是 ref,子元件仍會得到一個可響應的物件。

7. 多層 Provide / Override

// GrandParent.vue
<script setup>
import { provide, ref } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

provide(ThemeSymbol, ref('light'))   // 預設主題
</script>

<template>
  <ParentComponent />
</template>
// ParentComponent.vue
<script setup>
import { provide, ref, inject } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

const parentTheme = ref('dark')
provide(ThemeSymbol, parentTheme)   // 覆寫祖先提供的值
</script>

<template>
  <slot />
</template>

子元件會拿到最近的 provide,也就是 ParentComponent 所提供的 'dark',而不是 GrandParent'light'


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記提供 key inject 時找不到對應的 key,會得到 undefined,導致錯誤。 使用 Symbol 定義 key,並在同一檔案匯出,減少拼寫錯誤。
提供非響應式資料 直接 provide({ count: 0 }),子元件改變不會觸發更新。 提供 refreactivecomputed,確保子元件取得同一個響應式引用。
過度依賴 provide/inject 把大量全域狀態塞進 provide,會讓系統變成「隱形」依賴,難以追蹤。 僅用於 跨層級共享服務注入,全域狀態仍建議使用 Pinia。
在同一層級多次 provide 同一 key 後面的會覆蓋前面的,可能造成預期外的行為。 明確規劃哪一層負責提供,或使用不同的 key。
忘記在子元件 setup 中使用 inject 直接在模板中使用 inject 會失敗。 必須在 setup 中呼叫 inject,再回傳至模板。

最佳實踐

  1. 命名空間化:使用 src/injectionKeys.js 統一管理 Symbol,提升可讀性與維護性。
  2. 提供最小介面:只 expose 必要的 API(如 loginlogout),隱藏實作細節。
  3. 保持響應式:若需要共享狀態,務必以 ref / reactive 包裝,避免手動觸發更新。
  4. 加上預設值:在 inject 時提供 fallback,讓元件在孤立測試或未被包裹時仍能正常運作。
  5. 使用 TypeScript 時:為 inject 加上型別斷言或 as,提升開發體驗。
// example.ts
import { inject } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'

const theme = inject<Ref<string>>(ThemeSymbol)!

實際應用場景

1. 表單套件的欄位註冊

在自訂的表單元件庫中,父表單 需要收集所有子欄位的驗證規則與值。透過 provide('formContext', ctx),每個欄位在 setupinject('formContext'),即可自動向父表單註冊/取消註冊,無需手動傳遞 props。

2. 主題 (Theme) 系統

全站的暗黑 / 明亮模式只需要在根元件 provide(themeRef),所有子元件直接 inject(themeRef),即使深層的彈窗、工具列也能即時跟隨主題變更。

3. 多語系 (i18n) 注入

在大型專案中,常會在根元件提供 i18n 服務(例如 vue-i18nt 方法)。子元件只要 inject(i18nKey) 即可取得翻譯函式,避免在每個元件都 import i18n 實例。

4. 動態插件系統

假設有一個「插件容器」元件,允許外部程式碼在執行時注入自訂功能。容器 provide(pluginApi),插件內部 inject(pluginApi) 取得介面,實現 依賴反轉


總結

provide / inject 是 Vue3 Composition API 中的 跨層級依賴注入 機制,適合用來:

  • 共享響應式狀態(如主題、語系)
  • 注入服務或工具類別(API client、驗證服務)
  • 實作表單、插件等需要子元件自行註冊的功能

使用時應遵循以下要點:

  1. 以 Symbol 作為 key,避免衝突。
  2. 提供 ref / reactive,確保子元件能即時感知變更。
  3. 加上預設值,提升元件的容錯與可測試性。
  4. 限制使用範圍,全域狀態仍建議使用 Pinia 或 Vuex。

掌握了 provide / inject,你就能寫出結構更清晰、耦合度更低的 Vue3 應用,讓大型專案的維護與擴充變得更簡單。祝你開發順利,玩得開心!