Skip to content

在元件裡使用 store:storeToRefs

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

取得 store

元件要使用 store 時,先 import useXxxStore,再在 <script setup> 的同步階段呼叫它:

src/components/CartSummary.vue

vue
<script setup>
import { useCartStore } from '../stores/cart.js'

const cart = useCartStore()
</script>

<template>
  <section>
    <p>品項數:{{ cart.itemCount }}</p>
    <p>小計:{{ cart.total }}</p>
  </section>
</template>

useCartStore() 回傳的是 Pinia 的響應式 store proxy。元件不需要把 store 再包一層,也不需要把每個欄位傳過多層 props;直接讀 cart.itemCountcart.total,就能跟著 state 的變化重新渲染。多個元件呼叫同一個 store id 時,取得的是同一份 store 狀態。

不要在模組最外層建立 store:那個位置可能還沒有 active Pinia,尤其是 router 守衛或測試組裝尚未完成時。元件的 <script setup>、action 或 router hook 都是在應用程式已安裝 Pinia 後才執行的安全位置;auth store 與守衛的完整情境會在後面說明。

解構時保留響應性

直接解構 store 的 state 或 getter,會把值拿成當下的結果,失去原本和 store 的連線:

js
const cart = useCartStore()

// 錯誤:itemCount 不再跟著 cart 更新
const { itemCount, total } = cart

這個錯誤不一定會立刻出現在畫面上。元件第一次渲染可能看起來正常,等其他元件加入商品後,解構出來的值卻不更新。

正解是用 Pinia 提供的 storeToRefs(),只把 state 和 getter 轉成 refs;它會略過 action 和其他非響應式屬性:

src/components/CartSummary.vue

vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart.js'

const cart = useCartStore()
const { itemCount, total } = storeToRefs(cart)
</script>

<template>
  <section>
    <p>品項數:{{ itemCount }}</p>
    <p>小計:{{ total }}</p>
  </section>
</template>

模板會自動解開 ref,所以不需要寫 itemCount.value。如果是在 JavaScript 邏輯中讀取,則仍然要寫 itemCount.value。一個實用的分工是:state 和 getter 用 storeToRefs() 解構,action 直接從 store 解構。

常見錯誤

錯誤寫法是把整個 store 的欄位直接解構,然後以為它們仍然是響應式資料。原因是解構會切斷 store proxy 對 state 和 getter 的追蹤。正確寫法是 const { itemCount } = storeToRefs(cart);若不需要解構,也可以一直使用 cart.itemCount

把行為收進 action

action 是 store 對外提供的行為介面。元件可以直接解構 action,因為 action 本身不是需要透過 storeToRefs() 保持響應性的 ref:

js
const cart = useCartStore()
const { addItem, removeItem } = cart

addItem({ id: 1, title: '重新認識 Vue.js', price: 680 })
removeItem(1)

修改購物車的規則應該放在 addItemremoveItem 這些 action 裡。元件只描述使用者做了什麼,不應該自己查找相同書籍、增加數量或決定刪除索引。這樣未來加入頁面、測試或持久化時,仍然只有一個修改入口。

Pinia 允許直接修改 state,也提供 $patch() 做一次性的批次修改:

js
cart.items.push({ id: 2, title: '0 陷阱 JavaScript', price: 620, quantity: 1 })

cart.$patch((state) => {
  state.items = state.items.filter((item) => item.id !== 2)
})

直接修改適合簡單的表單綁定,$patch() 適合一組相關變更,兩者都能被 DevTools 追蹤。不過書店的業務規則仍建議收進 action,讓元件不必知道 state 的資料結構。開發時若發現徽章沒有更新,可以打開第一章介紹的 Vue DevTools,在 Pinia store 裡檢查 state 與 action 記錄。

書店場景串起來

最後把「加入後前往購物車」和「導覽列顯示徽章」放回同一個畫面。完成動作後才決定目的地時,使用 useRouter();固定的站內連結則使用 RouterLink。這延續第六章的 RouterLink 與程式化導航

詳情頁呼叫 action,成功後用具名路由前往購物車:

src/views/BookDetailView.vue

vue
<script setup>
import { useRouter } from 'vue-router'
import { useCartStore } from '../stores/cart.js'

const router = useRouter()
const { addItem } = useCartStore()

function addToCart() {
  addItem({ id: 1, title: '重新認識 Vue.js', price: 680 })
  router.push({ name: 'cart' })
}
</script>

<template>
  <button type="button" @click="addToCart">
    加入購物車並查看
  </button>
</template>

導覽列則用 storeToRefs() 取得響應式的 itemCount。當詳情頁的 action 修改同一個 store,這裡的徽章會自動重算:

src/components/SiteHeader.vue

vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart.js'

const cart = useCartStore()
const { itemCount } = storeToRefs(cart)
</script>

<template>
  <header>
    <RouterLink :to="{ name: 'home' }">書籍</RouterLink>
    <RouterLink :to="{ name: 'cart' }">
      購物車({{ itemCount }})
    </RouterLink>
  </header>
</template>

這裡有三個可以檢查的關係:BookDetailViewSiteHeader 取得同一個 useCartStore();詳情頁只呼叫 addItem,不自行改寫購物車規則;徽章讀取的是 storeToRefs(cart) 產生的 ref。只要其中一點被改成各自的 ref,畫面就不再共享同一份資料。

動手檢查

先在導覽列觀察徽章,再按詳情頁的按鈕。徽章應該先增加,接著 router 才導航到 cart 路由。若把 const { itemCount } = cart 暫時換回錯誤寫法,再從另一個元件加入商品,就能看見失去響應性的差異。

購物車的讀寫到這裡已經在元件裡跑順了。下一節把同一套模式交給登入狀態,和 Vue Router 的守衛整合。請接著閱讀auth store:與 Vue Router 整合