Vue 3 組合式函式(Composables)── 建立自訂 useXXX 函式
簡介
在 Vue 3 中,組合式 API(Composition API) 讓我們可以把「邏輯」抽離成可重用的函式,稱為 composable。傳統的 Options API 常把資料、方法與生命週期混雜在同一個 components 內,當功能逐漸增長時,程式碼很快會變得難以維護。
自訂的 useXXX 函式正是解決這個問題的關鍵:它把相關的 state、reactive 變數與副作用封裝在一起,讓多個元件可以 乾淨、簡潔 地共享同一套邏輯。
本篇文章將從 概念、實作、常見陷阱 以及 最佳實踐 逐步說明,讓你能在專案中快速上手並建立自己的 useXXX 函式。
核心概念
1. 為什麼要使用 useXXX?
- 邏輯重用:同一段功能(例如表單驗證、倒數計時、API 請求)可以在多個元件間共享。
- 關注點分離:把 UI(template)與業務邏輯分開,讓元件更聚焦於「呈現」。
- 更好的型別支援:在 TypeScript 專案中,composable 能提供清晰的型別定義。
2. 基本結構
一個最簡單的 composable 大致如下:
import { ref, computed, onMounted, onUnmounted } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const inc = () => count.value++
const dec = () => count.value--
const double = computed(() => count.value * 2)
return { count, inc, dec, double }
}
ref/reactive:建立可響應的資料。computed:衍生出基於其他響應式資料的值。- 生命週期 hook(如
onMounted)可在 composable 內使用,讓副作用與元件綁定。
3. 常見的 useXXX 範例
3.1 useFetch – 簡易的 HTTP 請求封裝
import { ref, onMounted } from 'vue'
import axios from 'axios'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await axios.get(url)
data.value = response.data
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 自動在組件掛載時執行
onMounted(fetchData)
return { data, error, loading, refetch: fetchData }
}
重點:
refetch讓呼叫端可以手動重新抓取資料;onMounted確保只在元件真正掛載後才發送請求。
3.2 useLocalStorage – 把資料同步至 LocalStorage
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue = null) {
const stored = localStorage.getItem(key)
const state = ref(stored ? JSON.parse(stored) : defaultValue)
// 當 state 改變時自動寫回 localStorage
watch(state, (newVal) => {
if (newVal === null) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newVal))
}
}, { deep: true })
return state
}
技巧:使用
watch並設定{ deep: true },可以偵測到物件內部屬性的變化。
3.3 useTimer – 倒數計時器
import { ref, onMounted, onUnmounted } from 'vue'
export function useTimer(seconds = 0) {
const time = ref(seconds)
let timerId = null
const start = () => {
if (timerId) return
timerId = setInterval(() => {
if (time.value > 0) {
time.value--
} else {
stop()
}
}, 1000)
}
const stop = () => {
clearInterval(timerId)
timerId = null
}
const reset = (newSec = seconds) => {
stop()
time.value = newSec
start()
}
onMounted(start)
onUnmounted(stop)
return { time, start, stop, reset }
}
說明:透過
onMounted與onUnmounted自動管理計時器的啟停,避免記憶體洩漏。
3.4 useDarkMode – 暗黑模式切換
import { ref, watchEffect } from 'vue'
export function useDarkMode() {
const isDark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
const toggle = () => {
isDark.value = !isDark.value
}
// 讓 <html> 的 class 隨著 isDark 改變
watchEffect(() => {
document.documentElement.classList.toggle('dark', isDark.value)
})
return { isDark, toggle }
}
重點:
watchEffect會自動追蹤isDark,只要值變動就會更新class。
3.5 useFormValidator – 表單驗證範例
import { reactive, computed } from 'vue'
export function useFormValidator(rules) {
const form = reactive({})
const errors = reactive({})
const validateField = (field) => {
const value = form[field]
const fieldRules = rules[field] || []
errors[field] = ''
for (const rule of fieldRules) {
const result = rule.validator(value)
if (!result) {
errors[field] = rule.message
break
}
}
}
const isValid = computed(() => {
return Object.values(errors).every((msg) => msg === '')
})
const validateAll = () => {
Object.keys(rules).forEach(validateField)
}
return { form, errors, validateField, validateAll, isValid }
}
說明:
rules以{ field: [{ validator, message }, ...] }的形式傳入,讓驗證邏輯完全抽離。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 直接在 composable 中使用全域變數 | 會讓測試變得困難,且在 SSR 時可能產生不一致的結果。 | 透過參數注入(dependency injection)或提供 ref/reactive 作為輸入。 |
| 忘記清理副作用 | 如 setInterval、eventListener、watch 等未在 onUnmounted 中釋放,會造成記憶體洩漏。 |
必須在 composable 內使用對應的生命週期 hook 來清理。 |
| 返回非 reactive 的值 | 若直接回傳普通物件,外部元件無法追蹤變化。 | 確保回傳的資料是 ref、reactive、computed 或是函式。 |
| 過度抽象 | 把太多不相關的邏輯塞進同一個 composable,會降低可讀性。 | 依照功能邊界拆分,保持 單一職責原則(SRP)。 |
| 缺少型別或文件 | 在大型團隊中,缺乏說明會讓新成員難以使用。 | 為每個 useXXX 撰寫 JSDoc 或 TypeScript 定義檔。 |
最佳實踐:
- 命名規則:所有 composable 必須以
use為前綴,並使用動詞或名詞描述功能(如useFetch、useLocalStorage)。 - 返回值結構:盡量返回一個 物件,而非陣列,讓呼叫端能以解構賦值的方式自行挑選所需屬性。
- 參數驗證:在函式入口處檢查傳入參數,避免因傳入錯誤類型導致 runtime error。
- 可測試性:把副作用(例如
axios、localStorage)抽成可注入的依賴,單元測試時可使用 mock。 - 文件說明:在每個 composable 的檔案最上方寫下簡短說明、使用範例與參數說明,提升團隊協作效率。
實際應用場景
| 場景 | 可能的 useXXX |
為什麼適合使用 composable |
|---|---|---|
| 多頁面的表單驗證 | useFormValidator |
同一套驗證規則可以在不同頁面共用,且維護集中。 |
| 即時資料串流(WebSocket) | useWebSocket(自行實作) |
連線、訊息處理與斷線重連等邏輯抽離,元件只負責 UI。 |
| 使用者偏好設定(主題、語系) | useUserSettings、useDarkMode |
讓所有元件即時感知設定變更,避免重複讀取 localStorage。 |
| 倒數計時或輪播圖 | useTimer、useCarousel |
時間相關的副作用集中管理,避免每個元件自行寫 setInterval。 |
| 資料快取(如搜尋結果快取) | useCache |
把快取策略(memory、sessionStorage)封裝,讓 API 呼叫更乾淨。 |
案例:假設我們在電商平台上有「商品列表」與「商品詳情」兩個頁面,都需要根據使用者的暗黑模式自動切換 UI。只要在兩個元件中
import { useDarkMode } from '@/composables/useDarkMode',即可共享同一套切換邏輯,且如果未來要加入「自動偵測系統時間」的功能,只需要在useDarkMode中加入對Date的判斷,所有使用此 composable 的元件立即受惠。
總結
- 組合式函式(Composable) 是 Vue 3 推薦的邏輯重用方式,透過
useXXX把 state、computed、life‑cycle 等封裝在一起,讓元件保持「純粹」的 UI 表現。 - 建立自訂
useXXX時,核心步驟是:定義 reactive 資料 → 實作業務邏輯 → 必要時加入生命週期 hook → 回傳可供外部解構的物件。 - 注意 清理副作用、保持單一職責、提供清晰文件,即可寫出易於維護、可測試且在團隊中易於共享的 composable。
- 透過本文提供的多個實用範例,你可以立即在專案中導入
useFetch、useLocalStorage、useTimer等常見功能,並依需求自行擴充更複雜的邏輯。
掌握了自訂 useXXX 的技巧,你將能以更模組化的方式開發 Vue 3 應用,提升開發效率與程式碼品質。祝你寫出乾淨、可重用的 Vue 3 程式碼! 🚀