本文 AI 產出,尚未審核

Vue3 Pinia 狀態管理 – 建立 Store(defineStore

簡介

在單頁應用(SPA)中,元件之間的資料共享是不可避免的議題。Vue3 原生提供的 props / emit 能解決父子關係的傳遞,但面對跨層級或全局共享的需求時,狀態管理 就顯得尤為重要。Pinia 是 Vue 官方推薦的下一代狀態管理庫,取代了過去的 Vuex,語法更簡潔、類型支援更好,同時與 Vue3 的 Composition API 天生相容。

本單元將聚焦在 建立 Store——最基礎也是最關鍵的步驟。透過 defineStore,我們可以把狀態、getter、action 整理成一個獨立的模組,在任何元件中以 useStore() 的方式直接使用,讓程式碼更易讀、易維護,也更符合「單一職責」的設計原則。


核心概念

1. 為什麼使用 defineStore

defineStore 是 Pinia 提供的 API,用來 定義一個 Store。它接受兩個必填參數:

參數 型別 說明
id string Store 的唯一名稱,會成為全域註冊的 key。
options object 包含 stategettersactions 的設定。

使用 defineStore 的好處包括:

  • 自動支援 TypeScript:Pinia 會根據 stategettersactions 推斷型別。
  • 熱更新友好:開發時可即時看到資料變動,無需重新載入頁面。
  • 模組化:每個 Store 都是獨立檔案,便於分割與測試。

2. 基本結構

// src/stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // 1. state:回傳一個包含預設值的物件
  state: () => ({
    count: 0
  }),

  // 2. getters:類似 computed,依賴於 state
  getters: {
    doubleCount: (state) => state.count * 2,
    // 若需要存取其他 getter,可使用 this
    tripleCount() {
      return this.doubleCount + this.count
    }
  },

  // 3. actions:可包含非同步邏輯,直接修改 state
  actions: {
    increment(step = 1) {
      this.count += step
    },
    async fetchInitialCount() {
      const res = await fetch('/api/init-count')
      const data = await res.json()
      this.count = data.value
    }
  }
})

重點state 必須是 函式,這樣每次呼叫 useCounterStore() 時都會得到一個獨立的狀態實例,避免共享同一個物件導致不可預期的行為。


3. 在元件中使用 Store

<template>
  <div>
    <p>計數:{{ counter.count }}</p>
    <p>雙倍:{{ counter.doubleCount }}</p>
    <button @click="counter.increment()">+1</button>
    <button @click="counter.increment(5)">+5</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

// 取得 Store 實例
const counter = useCounterStore()

// 若想在組件掛載時自動呼叫非同步 action
onMounted(() => {
  counter.fetchInitialCount()
})
</script>

說明

  • useCounterStore() 會回傳同一個 Store 實例(在同一個 Pinia 實例內),因此在多個元件間共享狀態非常直接。
  • 直接呼叫 counter.increment() 或存取 counter.doubleCount,Pinia 會自動保持 reactivity,元件會在值變動時重新渲染。

4. 多 Store 與命名空間

當專案規模變大時,建議依功能切分 Store,例如 usercartproduct。每個 Store 的 id 必須唯一,Pinia 會自動以 id 為鍵值管理。

// src/stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    profile: null
  }),
  getters: {
    isLoggedIn: (state) => !!state.token
  },
  actions: {
    login(payload) {
      this.token = payload.token
      this.profile = payload.user
    },
    logout() {
      this.token = ''
      this.profile = null
    }
  }
})

在元件中同時使用多個 Store:

import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const user = useUserStore()
const cart = useCartStore()

if (user.isLoggedIn) {
  cart.fetchCartItems()
}

5. Store 持久化(Persist)

在真實專案中,常需要在頁面刷新後保留使用者的狀態。Pinia 官方提供 pinia-plugin-persistedstate 插件,只要在 Store 中加入 persist: true(或自訂配置),即可自動將 state 存入 localStoragesessionStorage

// src/stores/theme.js
import { defineStore } from 'pinia'

export const useThemeStore = defineStore('theme', {
  state: () => ({
    darkMode: false
  }),
  actions: {
    toggle() {
      this.darkMode = !this.darkMode
    }
  },
  // 開啟持久化,預設使用 localStorage
  persist: true
})

提示:若只想持久化部分欄位,可使用 persist: { paths: ['darkMode'] }


常見陷阱與最佳實踐

陷阱 說明 解決方案
直接修改 state 之外的變數 actions 中若直接寫 count = 5(未加 this.),會變成局部變數,無法觸發 reactivity。 必須使用 this.count = 5this.$state.count = 5
state 不是函式 若寫成 state: { count: 0 },所有使用者會共享同一個物件,導致互相干擾。 永遠使用函式state: () => ({ count: 0 })
Getter 內部使用 async Getter 必須是同步的,若需要非同步計算,應改寫為 action 並把結果存回 state 使用 action + state,或在組件內使用 watchEffect
過度集中 Store 把所有狀態都塞進一個巨大的 Store,會讓維護變得困難。 功能拆分:每個領域(User、Cart、Product)各自一個 Store。
忘記註冊 Pinia main.js 中沒有 app.use(createPinia()),會導致 useStore() 拋錯。 在入口檔案初始化 Pinia,且只初始化一次。

最佳實踐

  1. 使用 TypeScript:即使是 JavaScript 專案,也建議加入 @pinia/nuxtpinia 的型別定義,提升開發體驗。
  2. 保持 Store 純粹:只放置資料與相關邏輯,避免把 UI 相關的程式碼(如 DOM 操作)寫在 Store 中。
  3. 利用插件:如 pinia-plugin-persistedstatepinia-plugin-logger,可以在不改動原始程式碼的情況下加入持久化或除錯功能。
  4. 單元測試:Pinia Store 天然支援測試,只要呼叫 setActivePinia(createPinia()) 即可在 Jest/Vitest 中模擬環境。
// vitest 測試範例
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

beforeEach(() => {
  setActivePinia(createPinia())
})

test('increment works', () => {
  const store = useCounterStore()
  store.increment(3)
  expect(store.count).toBe(3)
})

實際應用場景

  1. 使用者認證與權限

    • userStore 負責保存 JWT、使用者資訊以及登入/登出流程。
    • 其他需要驗證的頁面只要檢查 userStore.isLoggedIn,即可決定是否導向登入頁。
  2. 購物車系統

    • cartStore 包含商品列表、總金額、加減商品等 actions。
    • 透過持久化,使用者即使關閉瀏覽器也能保留購物車內容。
  3. 即時通知

    • notificationStore 用來管理訊息佇列,actions 內部可呼叫 WebSocket,收到訊息即 pushstate.notifications,元件自動渲染。
  4. 主題切換

    • themeStore 控制暗黑模式與亮色模式,結合 persist 後,使用者在不同頁面或重新載入時仍保持偏好。
  5. 多語系切換

    • localeStore 保存當前語系,actions 內部載入對應的 i18n 資源,所有元件透過 store.currentLocale 即可即時切換文字。

總結

defineStore 是 Pinia 的核心入口,透過簡潔的 id + options 結構,我們能快速建立具備 state、getter、action 的全域模組。正確使用函式式 state、遵守 reactivity 原則、適時拆分 Store,能讓專案在規模擴大時仍保持可維護性。結合插件(持久化、日志)與單元測試,更能提升開發效率與產品穩定度。

掌握了 建立 Store 後,你就擁有了 Vue3 應用中最強大的狀態管理工具,未來無論是簡單的計數器還是複雜的電商系統,都能以 Pinia 讓資料流動變得清晰、可靠。祝你在開發旅程中玩得開心,寫出更優雅的 Vue3 應用!