Vue3 組合式函式(Composables)— provide / inject 結合 composable
簡介
在 Vue 3 中,組合式 API(Composition API) 讓我們可以把「邏輯」抽離成可重用的函式(即 composable),進一步提升程式碼的可讀性與維護性。
然而,當多個元件之間需要共享同一份狀態或方法時,僅靠單純的 composable 仍不足以解決「跨層級傳遞」的問題。這時,Vue 內建的 provide / inject 機制就派上用場:父層透過 provide 暴露資料,子層(甚至更深層的孫子元件)則用 inject 取得。
把 provide / inject 與 composable 結合後,我們可以:
- 在根層或任意父元件一次性注入 複雜的業務邏輯(例如:表單驗證、國際化、權限控制)。
- 子元件只需要呼叫
useXxx()便能取得完整功能,保持了 composable 的「可插拔」特性。 - 避免層層傳遞 props,減少不必要的重新渲染與耦合度。
本篇文章將從核心概念說明開始,示範多個實務範例,最後列出常見陷阱與最佳實踐,幫助你在 Vue3 專案中熟練運用 provide / inject + composable 的寫法。
核心概念
1. 為什麼要把 provide / inject 包裝成 composable?
- 封裝性:直接在元件裡寫
provide、inject會讓程式碼散落在不同檔案;把它們包在一個 composable(例如useUserProvider())內,讓所有相關邏輯集中管理。 - 可重用:同一套提供/注入機制可以在多個父子組件樹中重複使用,只要在父層呼叫一次 composable,即可在任意子層取得。
- 型別安全(若使用 TypeScript):在 composable 中定義介面與預設值,可讓 IDE 提供自動完成與錯誤提示。
2. 基本語法回顧
| 方法 | 位置 | 用途 |
|---|---|---|
provide(key, value) |
父層 (setup) | 把 value 以 key 暴露給子孫 |
inject(key, defaultValue?) |
任意子層 (setup) | 取得先前 provide 的 value,若不存在則回傳 defaultValue |
註:
key可以是字串、Symbol,建議使用 Symbol 以避免命名衝突。
3. 建立 Provide Composable
以下示範一個 全域主題(Theme) 的 composable,讓任何子元件都能透過 useTheme() 取得與切換主題。
// src/composables/useThemeProvider.js
import { ref, provide, inject } from 'vue'
// 使用 Symbol 作為唯一 key
const ThemeSymbol = Symbol('Theme')
export function provideTheme() {
const theme = ref('light') // 預設主題
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// 把需要共享的資料與方法一起 provide
provide(ThemeSymbol, {
theme,
toggleTheme,
})
}
// 子層使用的 composable
export function useTheme() {
const ctx = inject(ThemeSymbol)
if (!ctx) {
throw new Error('useTheme must be called after provideTheme')
}
return ctx
}
重點:
provideTheme()只需要在根元件或任意想要「提供」主題的父層呼叫一次;之後的子元件只要useTheme()就能取得theme與toggleTheme。
4. 範例一:表單驗證服務
// src/composables/useFormValidator.js
import { reactive, provide, inject } from 'vue'
const FormValidatorSymbol = Symbol('FormValidator')
export function provideFormValidator(rules) {
const state = reactive({
errors: {},
})
function validate(field, value) {
const rule = rules[field]
if (!rule) return true // 沒有規則直接通過
const result = rule(value)
state.errors[field] = result ? '' : `Invalid ${field}`
return result
}
provide(FormValidatorSymbol, {
errors: state.errors,
validate,
})
}
export function useFormValidator() {
const ctx = inject(FormValidatorSymbol)
if (!ctx) {
throw new Error('useFormValidator must be used after provideFormValidator')
}
return ctx
}
使用方式:
<!-- ParentForm.vue -->
<script setup>
import { provideFormValidator } from '@/composables/useFormValidator'
const rules = {
email: v => /\S+@\S+\.\S+/.test(v),
password: v => v.length >= 6,
}
provideFormValidator(rules)
</script>
<template>
<slot />
</template>
<!-- ChildInput.vue -->
<script setup>
import { useFormValidator } from '@/composables/useFormValidator'
import { ref } from 'vue'
const { validate, errors } = useFormValidator()
const email = ref('')
function onBlur() {
validate('email', email.value)
}
</script>
<template>
<input v-model="email" @blur="onBlur" />
<p class="error">{{ errors.email }}</p>
</template>
說明:表單規則只在父層定義一次,所有子層的輸入框都能共享同一套驗證邏輯與錯誤狀態。
5. 範例二:權限管理(Permission)
// src/composables/usePermissionProvider.js
import { ref, provide, inject, computed } from 'vue'
const PermissionSymbol = Symbol('Permission')
export function providePermission(userRoles = []) {
const roles = ref(userRoles)
const hasPermission = (required) => {
if (Array.isArray(required)) {
return required.some(r => roles.value.includes(r))
}
return roles.value.includes(required)
}
// 讓子層可以直接用 computed 判斷
const can = (required) => computed(() => hasPermission(required))
provide(PermissionSymbol, {
roles,
hasPermission,
can,
})
}
export function usePermission() {
const ctx = inject(PermissionSymbol)
if (!ctx) {
throw new Error('usePermission must be called after providePermission')
}
return ctx
}
子元件使用:
<!-- AdminButton.vue -->
<script setup>
import { usePermission } from '@/composables/usePermissionProvider'
const { can } = usePermission()
const canDelete = can(['admin', 'moderator'])
</script>
<template>
<button v-if="canDelete" @click="deleteItem">刪除</button>
</template>
重點:
can()回傳computed,讓 Vue 能自動追蹤權限變化,UI 會即時更新。
6. 範例三:多語系(i18n)輕量實作
// src/composables/useI18nProvider.js
import { ref, provide, inject, computed } from 'vue'
const I18nSymbol = Symbol('I18n')
export function provideI18n(messages, defaultLocale = 'en') {
const locale = ref(defaultLocale)
function t(key) {
return messages[locale.value][key] ?? key
}
const translate = (key) => computed(() => t(key))
provide(I18nSymbol, {
locale,
t,
translate,
setLocale: (l) => (locale.value = l),
})
}
export function useI18n() {
const ctx = inject(I18nSymbol)
if (!ctx) {
throw new Error('useI18n must be used after provideI18n')
}
return ctx
}
在子元件中:
<!-- Greeting.vue -->
<script setup>
import { useI18n } from '@/composables/useI18nProvider'
const { t, locale, setLocale } = useI18n()
</script>
<template>
<p>{{ t('welcome') }}</p>
<select v-model="locale">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</template>
說明:切換
locale後,所有使用t()或translate()的文字會即時重新計算,達到「全局多語」的效果。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記在父層呼叫 provide composable | 子層 inject 會得到 undefined,導致程式錯誤。 |
在 composable 中加入 防呆檢查(如上例的 if (!ctx) throw new Error(...)),並在文件說明「必須先呼叫 provideXxx()」。 |
| 使用字串作為 key,產生衝突 | 多個不同的 provide 可能意外使用相同字串,導致資料被覆寫。 | 建議使用 Symbol 作為唯一的 key。 |
provide 的值是原始資料,未使用 ref/reactive |
子層取得後不會自動響應變化。 | 把需要響應的狀態包在 ref、reactive 或 computed 中再提供。 |
| 過度依賴 provide / inject,導致耦合 | 當層級結構改變時,子層仍依賴舊的提供者,維護成本升高。 | 保持 composable 的獨立性:在同一層級或同一功能模組內使用,若跨模組使用,可考慮使用全局 store(Pinia)或插件。 |
| Inject 的預設值不是同型別 | 若預設值與真正提供的類型不一致,會在 TypeScript 中產生錯誤。 | 為 inject 指定正確的型別或使用 as 斷言,或在提供者缺失時拋出錯誤。 |
最佳實踐
- 封裝成 composable:
provideXxx()與useXxx()成對出現,讓 API 明確。 - 使用 Symbol 作為 key,避免命名衝突。
- 把所有可變狀態包在
ref/reactive,確保子層可以自動追蹤。 - 在提供者內部加入錯誤檢查,讓開發者在開發階段即發現問題。
- 盡量保持單一職責:一個 composable 只負責一件事(如 Theme、Permission),避免「巨型」provide。
實際應用場景
| 場景 | 為什麼適合使用 provide / inject + composable |
|---|---|
| 大型表單(多個子欄位、跨頁) | 透過 provideFormValidator 集中驗證規則與錯誤狀態,子欄位不必傳遞大量 props。 |
| 主題切換(全站暗色/亮色) | provideTheme 只在根元件呼叫一次,所有子元件都能即時取得 theme,且不需要每層都傳遞 prop。 |
| 權限控制(功能按鈕、路由守衛) | providePermission 把角色資訊注入全局,子元件只要 usePermission 判斷即可,減少重複的 store 查詢。 |
| 多語系(i18n) | provideI18n 把語系字典與切換函式提供給整個應用,子元件只需要 t('key'),不必再自行 import 文字檔。 |
| 插件式功能(如拖曳、動畫) | 需要在多個元件之間共享同一個控制器(例如 dragController),可透過 composable 包裝後提供,保持插件的可插拔性。 |
總結
- provide / inject 為 Vue 3 的跨層級資料傳遞提供了原生支援,結合 composable 後,能把「提供」與「使用」的邏輯完整封裝,提升程式碼的可讀性與可維護性。
- 使用 Symbol 作為鑑別鍵、把狀態包在 ref / reactive、以及在 composable 中加入 防呆檢查,是避免常見陷阱的關鍵。
- 透過本篇示範的 主題、表單驗證、權限、i18n 四個實務範例,你可以快速在自己的專案中導入「provide + composable」的模式,減少 props 傳遞、提升 UI 的即時反應能力。
只要遵循 封裝、單一職責、型別安全 的原則,provide / inject + composable 將成為你在 Vue 3 中打造大型、可擴充應用的利器。祝開發順利,玩得開心!