本文 AI 產出,尚未審核

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 }
}

說明:透過 onMountedonUnmounted 自動管理計時器的啟停,避免記憶體洩漏。

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 作為輸入。
忘記清理副作用 setIntervaleventListenerwatch 等未在 onUnmounted 中釋放,會造成記憶體洩漏。 必須在 composable 內使用對應的生命週期 hook 來清理。
返回非 reactive 的值 若直接回傳普通物件,外部元件無法追蹤變化。 確保回傳的資料是 refreactivecomputed 或是函式。
過度抽象 把太多不相關的邏輯塞進同一個 composable,會降低可讀性。 依照功能邊界拆分,保持 單一職責原則(SRP)
缺少型別或文件 在大型團隊中,缺乏說明會讓新成員難以使用。 為每個 useXXX 撰寫 JSDoc 或 TypeScript 定義檔。

最佳實踐

  1. 命名規則:所有 composable 必須以 use 為前綴,並使用動詞或名詞描述功能(如 useFetchuseLocalStorage)。
  2. 返回值結構:盡量返回一個 物件,而非陣列,讓呼叫端能以解構賦值的方式自行挑選所需屬性。
  3. 參數驗證:在函式入口處檢查傳入參數,避免因傳入錯誤類型導致 runtime error。
  4. 可測試性:把副作用(例如 axioslocalStorage)抽成可注入的依賴,單元測試時可使用 mock。
  5. 文件說明:在每個 composable 的檔案最上方寫下簡短說明、使用範例與參數說明,提升團隊協作效率。

實際應用場景

場景 可能的 useXXX 為什麼適合使用 composable
多頁面的表單驗證 useFormValidator 同一套驗證規則可以在不同頁面共用,且維護集中。
即時資料串流(WebSocket) useWebSocket(自行實作) 連線、訊息處理與斷線重連等邏輯抽離,元件只負責 UI。
使用者偏好設定(主題、語系) useUserSettingsuseDarkMode 讓所有元件即時感知設定變更,避免重複讀取 localStorage。
倒數計時或輪播圖 useTimeruseCarousel 時間相關的副作用集中管理,避免每個元件自行寫 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。
  • 透過本文提供的多個實用範例,你可以立即在專案中導入 useFetchuseLocalStorageuseTimer 等常見功能,並依需求自行擴充更複雜的邏輯。

掌握了自訂 useXXX 的技巧,你將能以更模組化的方式開發 Vue 3 應用,提升開發效率與程式碼品質。祝你寫出乾淨、可重用的 Vue 3 程式碼! 🚀