Vue3 Pinia 狀態管理 – storeToRefs() 的用途
簡介
在 Vue3 生態系中,Pinia 已成為官方推薦的狀態管理解決方案。相較於 Vuex,Pinia 提供了更直觀的 API、支援 TypeScript,且與 Composition API 天生相容。
在實作組件時,我們常會把 store 直接解構(例如 const { count, increase } = useCounterStore()),但這樣會失去 響應式,導致 UI 不會自動更新。
storeToRefs() 正是為了解決這個問題而設計的工具函式:它能把 store 中的 state 轉換成 ref,讓解構後的屬性仍保持響應式,且不會破壞原始 store 的行為。本文將深入說明 storeToRefs() 的原理、使用方式,以及在實務開發中的最佳做法。
核心概念
1. 為什麼需要 storeToRefs()
在 Composition API 中,我們常用解構賦值取得 store 的屬性或方法:
const counterStore = useCounterStore()
const { count, increase } = counterStore // ← 直接解構
此時 count 只是一個普通的值(非 ref),如果在組件內部直接使用 count,Vue 無法追蹤其變化,畫面不會重新渲染。storeToRefs() 會把所有 state(包括 getters)包裝成 ref,保留其響應式特性,同時讓我們仍能使用解構寫法,提升程式碼可讀性。
2. storeToRefs() 的基本語法
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
count:變成ref<number>,在模板或watch中使用時會自動追蹤變化。doubleCount:若是 getter,同樣會被包裝成computed(也是一種ref),保持計算結果的即時更新。
注意:
storeToRefs()只會包裝 state 與 getters,actions 仍以原始函式形式返回,無需額外處理。
3. storeToRefs() 的實作原理(簡要說明)
Pinia 在內部維護一個 reactive 的 store 物件。storeToRefs() 透過 toRefs()(Vue 的 API)將每個屬性轉為 ref,再回傳一個新物件。這樣做的好處:
- 不改變原始 store:原始 store 仍是完整的 reactive 物件,其他未使用
storeToRefs()的地方不會受到影響。 - 避免多餘的 Proxy:直接使用
storeToRefs()產生的ref會比computed(() => store.xxx)更高效,因為它不需要每次存取時都走 Proxy。
4. 常見的使用情境
4.1 在 <script setup> 中解構 state
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
function handleClick() {
counterStore.increase()
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="handleClick">+1</button>
</div>
</template>
4.2 在 watch 中觀察多個 state
import { watch } from 'vue'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { name, age } = storeToRefs(userStore)
watch([name, age], ([newName, newAge]) => {
console.log(`使用者資訊變更:${newName}, ${newAge}`)
})
4.3 搭配 computed 建立衍生資料
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cartStore = useCartStore()
const { items } = storeToRefs(cartStore)
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
})
4.4 在 TypeScript 中保留類型
import { storeToRefs } from 'pinia'
import { useProductStore } from '@/stores/product'
const productStore = useProductStore()
const { list, selectedId } = storeToRefs(productStore)
// list: Ref<Product[]>
// selectedId: Ref<number | null>
4.5 與 mapStores、mapState 混用(非必要,但有時會遇到)
import { mapStores } from 'pinia'
export default {
computed: {
...mapStores(useAuthStore, ['authStore']),
// 仍需使用 storeToRefs 讓解構後的 state 保持響應式
...storeToRefs(this.authStore)
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
| 直接解構 state | const { count } = useCounterStore() 會失去響應式。 |
使用 storeToRefs() 包裝後再解構。 |
| 忘記保留 actions | storeToRefs() 不會返回 actions,若直接寫 const { increase } = storeToRefs(store) 會得到 undefined。 |
仍從原始 store 取得 actions:const { increase } = counterStore。 |
在模板中使用 .value |
ref 必須在模板外使用 .value,但在模板內 Vue 會自動解包。 |
在 <template> 中直接寫 {{ count }},在 <script> 中使用 .value。 |
| 過度解構導致大量 refs | 每次呼叫 storeToRefs() 都會產生新的 refs,若在大量組件中重複呼叫,可能造成不必要的記憶體開銷。 |
將 storeToRefs() 的結果儲存在組件的最上層(如 setup()),避免在循環或條件式內重複呼叫。 |
混用 toRefs 與 storeToRefs |
兩者功能相似但來源不同,混用會讓程式碼可讀性下降。 | 統一使用 storeToRefs(),除非你確定要對非 Pinia store 使用 toRefs。 |
最佳實踐
- 統一使用
storeToRefs:在所有需要解構 state 的地方,先呼叫一次storeToRefs,再從返回的物件中取值。 - 保留 actions 的原始引用:不要把 actions 包進
storeToRefs,直接從 store 本身取得。 - 在模板中直接使用:Vue 會自動把
ref解包,寫起來更簡潔。 - 利用 TypeScript:
storeToRefs會保留原始 store 的類型資訊,幫助 IDE 提供正確的自動完成與檢查。 - 避免在
watchEffect內直接解構:若在watchEffect中使用解構的ref,要確保使用.value,或直接使用原始 store。
實際應用場景
1. 表單資料雙向綁定
在大型表單中,我們常把表單狀態放到 Pinia store,以便跨頁保存。使用 storeToRefs 可以讓每個欄位直接綁定 ref,同時保持即時同步。
<script setup>
import { useFormStore } from '@/stores/form'
import { storeToRefs } from 'pinia'
const formStore = useFormStore()
const { username, email, phone } = storeToRefs(formStore)
</script>
<template>
<input v-model="username" placeholder="使用者名稱">
<input v-model="email" placeholder="電子信箱">
<input v-model="phone" placeholder="電話">
</template>
2. 多頁面共享的購物車
購物車的商品清單、總金額等資訊在不同頁面(商品列表、結帳頁)都需要即時顯示。storeToRefs 讓每個頁面只需要一次引用,UI 自動更新。
// cartStore.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[]
}),
getters: {
total(state) {
return state.items.reduce((sum, i) => sum + i.price * i.qty, 0)
}
},
actions: {
add(item) { this.items.push(item) },
clear() { this.items = [] }
}
})
在任意頁面:
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cartStore = useCartStore()
const { items, total } = storeToRefs(cartStore)
</script>
<template>
<div v-for="item in items" :key="item.id">
{{ item.name }} x {{ item.qty }}
</div>
<p>總金額:{{ total }}</p>
</template>
3. 動態權限控制
權限資訊往往在登入後一次性載入,之後各個功能元件根據權限顯示或隱藏。使用 storeToRefs 把權限列表轉成 ref,即使在深層子組件中也能自動更新。
// authStore.js
export const useAuthStore = defineStore('auth', {
state: () => ({
roles: [] as string[]
}),
getters: {
isAdmin: (state) => state.roles.includes('admin')
}
})
<script setup>
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
const authStore = useAuthStore()
const { isAdmin } = storeToRefs(authStore)
</script>
<template>
<button v-if="isAdmin">管理員設定</button>
</template>
總結
storeToRefs() 是 Pinia 在 Vue3 時代為 Composition API 量身打造的便利函式,它解決了 解構 state 後失去響應式 的痛點,讓開發者可以以更簡潔、易讀的方式使用 store。
透過本篇文章,我們了解了:
- 為什麼直接解構會失效,以及
storeToRefs如何將 state 包裝成ref。 - 基本語法與在
<script setup>、watch、computed、TypeScript 中的實作範例。 - 常見的陷阱(如忘記 actions、重複呼叫)與相應的最佳實踐。
- 在表單、購物車、權限控制等真實場景中的應用方式。
在日常開發中,只要遵循「先 storeToRefs,再解構」的原則,就能確保 UI 隨 state 變化即時更新,並且保持程式碼的可維護性。希望你在使用 Pinia 時,能把 storeToRefs() 當作日常工具,讓 Vue3 應用變得更順手、更可靠。祝開發愉快!