Vue3 Composition API(核心) – provide / inject
簡介
在大型 Vue3 應用中,元件之間的資料傳遞往往不只是一層或兩層的父子關係。若僅靠 props / emit,會讓中間層元件變成「傳遞屬性」(prop‑drilling) 的搬運工,增加維護成本。
Composition API 為我們提供了 provide 與 inject 這對 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 若是
ref、reactive或computed,子元件取得的仍是同一個響應式物件,變更會自動同步。
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 }),子元件改變不會觸發更新。 |
提供 ref、reactive 或 computed,確保子元件取得同一個響應式引用。 |
| 過度依賴 provide/inject | 把大量全域狀態塞進 provide,會讓系統變成「隱形」依賴,難以追蹤。 |
僅用於 跨層級共享 或 服務注入,全域狀態仍建議使用 Pinia。 |
| 在同一層級多次 provide 同一 key | 後面的會覆蓋前面的,可能造成預期外的行為。 | 明確規劃哪一層負責提供,或使用不同的 key。 |
忘記在子元件 setup 中使用 inject |
直接在模板中使用 inject 會失敗。 |
必須在 setup 中呼叫 inject,再回傳至模板。 |
最佳實踐
- 命名空間化:使用
src/injectionKeys.js統一管理 Symbol,提升可讀性與維護性。 - 提供最小介面:只 expose 必要的 API(如
login、logout),隱藏實作細節。 - 保持響應式:若需要共享狀態,務必以
ref/reactive包裝,避免手動觸發更新。 - 加上預設值:在
inject時提供 fallback,讓元件在孤立測試或未被包裹時仍能正常運作。 - 使用 TypeScript 時:為
inject加上型別斷言或as,提升開發體驗。
// example.ts
import { inject } from 'vue'
import { ThemeSymbol } from '@/injectionKeys'
const theme = inject<Ref<string>>(ThemeSymbol)!
實際應用場景
1. 表單套件的欄位註冊
在自訂的表單元件庫中,父表單 需要收集所有子欄位的驗證規則與值。透過 provide('formContext', ctx),每個欄位在 setup 中 inject('formContext'),即可自動向父表單註冊/取消註冊,無需手動傳遞 props。
2. 主題 (Theme) 系統
全站的暗黑 / 明亮模式只需要在根元件 provide(themeRef),所有子元件直接 inject(themeRef),即使深層的彈窗、工具列也能即時跟隨主題變更。
3. 多語系 (i18n) 注入
在大型專案中,常會在根元件提供 i18n 服務(例如 vue-i18n 的 t 方法)。子元件只要 inject(i18nKey) 即可取得翻譯函式,避免在每個元件都 import i18n 實例。
4. 動態插件系統
假設有一個「插件容器」元件,允許外部程式碼在執行時注入自訂功能。容器 provide(pluginApi),插件內部 inject(pluginApi) 取得介面,實現 依賴反轉。
總結
provide / inject 是 Vue3 Composition API 中的 跨層級依賴注入 機制,適合用來:
- 共享響應式狀態(如主題、語系)
- 注入服務或工具類別(API client、驗證服務)
- 實作表單、插件等需要子元件自行註冊的功能
使用時應遵循以下要點:
- 以 Symbol 作為 key,避免衝突。
- 提供
ref/reactive,確保子元件能即時感知變更。 - 加上預設值,提升元件的容錯與可測試性。
- 限制使用範圍,全域狀態仍建議使用 Pinia 或 Vuex。
掌握了 provide / inject,你就能寫出結構更清晰、耦合度更低的 Vue3 應用,讓大型專案的維護與擴充變得更簡單。祝你開發順利,玩得開心!