Skip to content

Pinia 状态管理

Pinia 是 Vue 官方推荐的状态管理库,是 Vuex 的继任者。但"该不该用 Pinia"比"怎么用 Pinia"更值得想清楚:很多项目把本该放在组件里的状态硬塞进 store,结果是数据流混乱、组件过度耦合。这一篇讲边界和正确用法。

先判断:真的需要 Pinia 吗

状态类型放哪里
组件自己用的输入框值、临时展开状态组件内部 ref / reactive
父子之间传递的数据props + emits
兄弟组件或跨层级共享、且会被多处修改Pinia store
登录态、用户信息、全局配置Pinia store
一次性请求的服务端数据组件内 + 请求库(如 TanStack Query)

判断标准:状态是否需要被多个彼此无父子关系的组件读写。是,就用 store;否,就留在组件里。

定义一个 store

Pinia 用 defineStore 创建 store。推荐用 setup 写法(组合式风格),它和 <script setup> 一致,类型推导也更好:

js
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  const profile = ref(null)
  const token = ref('')

  const isLoggedIn = computed(() => !!token.value)
  const displayName = computed(() => profile.value?.name ?? '游客')

  async function login(credentials) {
    const { token: t, profile: p } = await userApi.login(credentials)
    token.value = t
    profile.value = p
  }

  function logout() {
    token.value = ''
    profile.value = null
  }

  return { profile, token, isLoggedIn, displayName, login, logout }
})

在组件里用:

vue
<script setup>
import { useUserStore } from '@/stores/user'
const user = useUserStore()
// 注意:从 store 解构 state/getter 要用 storeToRefs 保持响应式
import { storeToRefs } from 'pinia'
const { isLoggedIn, displayName } = storeToRefs(user)
// action 可以直接解构
const { login } = user
</script>

解构 state 会丢响应式,必须用 storeToRefs。action 是普通函数,解构没问题。

state / getter / action 各管一摊

概念对应职责
stateref / reactive存储数据
gettercomputed派生值,带缓存
action普通函数(可 async)修改 state、处理副作用、调接口

getter 不要有副作用,它只描述"从 state 派生出来的值"。修改 state 的工作放在 action 里,调用方调 action 即可,不需要知道内部结构。

持久化:别什么都持久化

需要刷新后保留的(token、用户偏好、主题),持久化到 localStorage;临时性的(列表筛选、加载状态)不要持久化。常见做法是在 action 里手动写,或用 pinia-plugin-persistedstate

js
defineStore('user', () => { /* ... */ }, {
  persist: {
    paths: ['token', 'theme'] // 只持久化这些字段
  }
})

整个 store 无脑持久化会让旧版本数据残留,导致升级后页面异常。给持久化数据加版本号,升级时清理。

多 store 协作

store 之间可以互相调用:

js
// stores/cart.js
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  async function checkout() {
    const user = useUserStore()
    if (!user.isLoggedIn) throw new Error('请先登录')
    return orderApi.create({ items: items.value, token: user.token })
  }

  return { items, checkout }
})

避免循环依赖。两个 store 互相 import 通常是拆分粒度问题——把共享部分抽到第三个 store。

与组件状态的关系

store 不是"更高级的 ref"。组件自己用一次的数据放组件里,别为了"统一管理"全塞进 store。一个判断技巧:如果某段状态从 store 里拿出来,组件行为完全不变,那它本来就不该在 store 里。

SSR 注意

Nuxt / SSR 场景下,store 是单例还是每请求一个,决定了是否会串数据。Pinia 在 SSR 下要求每次请求创建新的 pinia 实例,避免不同用户共享同一份 state。Nuxt 集成 Pinia 时它会自动处理,但如果你手动用 pinia-plugin-* 之类的插件,要确认插件本身支持 SSR,否则会在服务端泄漏到下一个请求。

检查表

  • 先判断是否真需要跨组件共享,能放组件就放组件。
  • setup 写法定义 store,state/getter/action 各司其职。
  • 解构 state 用 storeToRefs,action 直接解构。
  • 持久化只保留真正需要的字段,并加版本号便于升级清理。
  • SSR 下每请求新建 pinia 实例,插件确认支持 SSR。

为复用而记录,为理解而整理。