本文 AI 產出,尚未審核

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() 只會包裝 stategettersactions 仍以原始函式形式返回,無需額外處理。

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 與 mapStoresmapState 混用(非必要,但有時會遇到)

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()),避免在循環或條件式內重複呼叫。
混用 toRefsstoreToRefs 兩者功能相似但來源不同,混用會讓程式碼可讀性下降。 統一使用 storeToRefs(),除非你確定要對非 Pinia store 使用 toRefs

最佳實踐

  1. 統一使用 storeToRefs:在所有需要解構 state 的地方,先呼叫一次 storeToRefs,再從返回的物件中取值。
  2. 保留 actions 的原始引用:不要把 actions 包進 storeToRefs,直接從 store 本身取得。
  3. 在模板中直接使用:Vue 會自動把 ref 解包,寫起來更簡潔。
  4. 利用 TypeScriptstoreToRefs 會保留原始 store 的類型資訊,幫助 IDE 提供正確的自動完成與檢查。
  5. 避免在 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>watchcomputed、TypeScript 中的實作範例。
  • 常見的陷阱(如忘記 actions、重複呼叫)與相應的最佳實踐。
  • 在表單、購物車、權限控制等真實場景中的應用方式。

在日常開發中,只要遵循「storeToRefs,再解構」的原則,就能確保 UI 隨 state 變化即時更新,並且保持程式碼的可維護性。希望你在使用 Pinia 時,能把 storeToRefs() 當作日常工具,讓 Vue3 應用變得更順手、更可靠。祝開發愉快!