Skip to content

Vue 3 實戰筆記:打造您的第一個 useCounter Composable

Vue 3 計數器:useCounter Composable

在 Vue 專案中,計數器功能雖然簡單,卻是理解 Composable (組合式函數) 最好的切入點。透過將計數邏輯從 UI 中抽離,我們能讓元件變得更純粹,並實現邏輯的跨元件共享。設計原則可先參考 Composable 介紹

為什麼需要抽離邏輯?

在傳統的單一檔案元件 (SFC) 中,我們常會這樣寫:

vue
// App.vue
<script setup>
import { ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}

function decrement() {
  count.value--;
}

function reset() {
  count.value = 0;
}
</script>

<template>
  <p>{{ count }}</p>
  <input @click="increment" type="button" value="increment">
  <input @click="decrement" type="button" value="decrement">
  <input @click="reset" type="button" value="reset">
</template>

這段程式碼在單一頁面運作良好,但如果需要在購物車數量、分頁控制器或點擊計數器中重複這些邏輯,重複撰寫相同的 ref() 與函數就會顯得冗餘。

定義 useCounter Composable

遵循 Vue 3 的開發規範,我們在 composables 目錄下建立一個 use 開頭的檔案。我們不只是移動程式碼,而是將其封裝成一個具備單一職責的黑盒子。

實作 useCounter.js

js
// composables/useCounter.js
import { ref } from 'vue';

export default () => {
  const count = ref(0);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  function reset() {
    count.value = 0;
  }

  return { count, increment, decrement, reset };
};

為什麼回傳物件而不是陣列?

回傳物件時,呼叫端可以 只取需要的項目,也能自由改名,日後新增回傳值也不會影響既有程式碼。陣列則必須照順序取值,多一個回傳值就得改所有呼叫處。

在元件中輕量化引用

重構後,原本臃腫的元件程式碼縮減為簡單的呼叫。這就是 Composable 的魅力:UI 歸 UI,邏輯歸邏輯。

vue
// App.vue
<script setup>
import useCounter from "@/composables/useCounter";

const { count, increment, decrement, reset } = useCounter();
</script>

<template>
  <p>{{ count }}</p>
  <input @click="increment" type="button" value="increment">
  <input @click="decrement" type="button" value="decrement">
  <input @click="reset" type="button" value="reset">
</template>

這裡解構為什麼不會失去響應式?

因為回傳的是 ref 物件本身,解構只是把同一個物件交給另一個變數,內部的 value 仍由 Vue 追蹤。這與解構 reactive 物件或 Pinia 的 store 會斷開連結的情況不同,後者才需要 storeToRefs

再進一步:讓它接受參數

只能從 0 開始、沒有上下限的計數器用途有限。Composable 真正的價值在於 同一份邏輯適用於不同場景

js
// composables/useCounter.js
import { ref, computed } from 'vue';

export default (initial = 0, { min = -Infinity, max = Infinity } = {}) => {
  const count = ref(initial);

  const isMax = computed(() => count.value >= max);
  const isMin = computed(() => count.value <= min);

  function increment() {
    if (!isMax.value) count.value++;
  }

  function decrement() {
    if (!isMin.value) count.value--;
  }

  function reset() {
    count.value = initial;
  }

  return { count, isMax, isMin, increment, decrement, reset };
};

購物車數量與分頁控制就能共用同一個函式,只是傳入的設定不同:

js
// 購物車數量:1 到庫存量
const { count, isMax, increment } = useCounter(1, { min: 1, max: stock });

// 分頁控制:從第 1 頁開始
const { count: page, increment: nextPage } = useCounter(1, { min: 1 });

isMaxisMin 這種衍生資料交給 computed 計算,比在每個方法裡各自判斷更不容易出錯,模板也能直接拿它來停用按鈕。

深度解析:Composable 的優勢

高度的邏輯複用性

useCounter 不再侷限於特定的元件。任何需要計數功能的頁面,只需要一行 import 即可立即獲得完整的狀態管理。

獨立的響應式狀態

這點至關重要:每當您在一個新元件中呼叫 useCounter(),Vue 都會建立一個全新的 count 實體。這意味著元件 A 與元件 B 的數值是獨立運作的,互不干擾。

真的希望多個元件共用同一個數字,做法是把 ref 移到函式外層,或者直接改用 Pinia 管理,語意上也更清楚。

符合單一職責原則 (SRP)

元件現在只負責 顯示數據觸發事件 ,而複雜的運算與邏輯變更全部由 Composable 處理。

常見問題

兩個元件都呼叫 useCounter 會共用同一個數字嗎?

不會。每次呼叫都會執行一次函式並建立新的 ref,因此各元件的計數彼此獨立。若真的需要共用,請把 ref 移到函式外層,或改用 Pinia

解構出來的 count 為什麼還有響應式?

因為回傳的是 ref 物件本身,解構只是把同一個物件交給另一個變數,內部的 value 仍由 Vue 追蹤。這與解構 reactive 物件會失去響應式的情況不同。

Composable 可以接受參數嗎?

可以,這也是它比複製貼上更有價值的原因。像 useCounter 可以接受初始值與上下限,讓同一份邏輯適用於購物車數量、分頁控制等不同場景。

回傳物件還是陣列比較好?

建議回傳物件。呼叫端可以只解構需要的項目,也能自由改名,新增回傳值時不會影響既有程式碼;陣列則必須依照順序取值。

在 Composable 裡可以用 computed 嗎?

可以,衍生資料就該用 computed。例如:是否已達上限這種判斷,交給 computed 自動計算,比在每個方法裡重複判斷更可靠。

延伸閱讀