本文 AI 產出,尚未審核

Vue3 組合式函式(Composables)— provide / inject 結合 composable


簡介

在 Vue 3 中,組合式 API(Composition API) 讓我們可以把「邏輯」抽離成可重用的函式(即 composable),進一步提升程式碼的可讀性與維護性。
然而,當多個元件之間需要共享同一份狀態或方法時,僅靠單純的 composable 仍不足以解決「跨層級傳遞」的問題。這時,Vue 內建的 provide / inject 機制就派上用場:父層透過 provide 暴露資料,子層(甚至更深層的孫子元件)則用 inject 取得。

provide / injectcomposable 結合後,我們可以:

  • 在根層或任意父元件一次性注入 複雜的業務邏輯(例如:表單驗證、國際化、權限控制)。
  • 子元件只需要呼叫 useXxx() 便能取得完整功能,保持了 composable 的「可插拔」特性。
  • 避免層層傳遞 props,減少不必要的重新渲染與耦合度。

本篇文章將從核心概念說明開始,示範多個實務範例,最後列出常見陷阱與最佳實踐,幫助你在 Vue3 專案中熟練運用 provide / inject + composable 的寫法。


核心概念

1. 為什麼要把 provide / inject 包裝成 composable?

  • 封裝性:直接在元件裡寫 provideinject 會讓程式碼散落在不同檔案;把它們包在一個 composable(例如 useUserProvider())內,讓所有相關邏輯集中管理。
  • 可重用:同一套提供/注入機制可以在多個父子組件樹中重複使用,只要在父層呼叫一次 composable,即可在任意子層取得。
  • 型別安全(若使用 TypeScript):在 composable 中定義介面與預設值,可讓 IDE 提供自動完成與錯誤提示。

2. 基本語法回顧

方法 位置 用途
provide(key, value) 父層 (setup) valuekey 暴露給子孫
inject(key, defaultValue?) 任意子層 (setup) 取得先前 providevalue,若不存在則回傳 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() 就能取得 themetoggleTheme

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 子層取得後不會自動響應變化。 把需要響應的狀態包在 refreactivecomputed 中再提供。
過度依賴 provide / inject,導致耦合 當層級結構改變時,子層仍依賴舊的提供者,維護成本升高。 保持 composable 的獨立性:在同一層級或同一功能模組內使用,若跨模組使用,可考慮使用全局 store(Pinia)或插件。
Inject 的預設值不是同型別 若預設值與真正提供的類型不一致,會在 TypeScript 中產生錯誤。 inject 指定正確的型別或使用 as 斷言,或在提供者缺失時拋出錯誤。

最佳實踐

  1. 封裝成 composableprovideXxx()useXxx() 成對出現,讓 API 明確。
  2. 使用 Symbol 作為 key,避免命名衝突。
  3. 把所有可變狀態包在 ref/reactive,確保子層可以自動追蹤。
  4. 在提供者內部加入錯誤檢查,讓開發者在開發階段即發現問題。
  5. 盡量保持單一職責:一個 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 中打造大型、可擴充應用的利器。祝開發順利,玩得開心!