Skip to content

Vuex 与 Pinia:状态管理的演进、差异与选型 ​

很多人第一次接触 Vuex 或 Pinia 时,最容易把它们理解成:Vue 项目里用来“全局存东西”的库。

这不算错,但太浅。

实际写项目时,更多是这样:状态管理工具解决的是“共享状态怎么组织、怎么约束、怎么让复杂页面和多个组件协作时不失控”的问题。

1. 为什么 Vue 应用会需要状态管理 ​

在一个小组件里,状态通常很简单:

  1. 本地输入值
  2. 当前选中项
  3. 一个弹窗开关

这类状态直接写在组件里就够了。

但项目一旦变复杂,就会出现这些问题:

  1. 多个组件都依赖同一份数据
  2. 数据要跨页面共享
  3. 用户信息、权限、主题、缓存条件需要统一管理
  4. 状态更新链路越来越难追

这时真正的问题不是“状态放哪”,而是:有没有一套更稳定的方式,让共享状态的来源、修改和消费都更可控。

2. 先分清什么状态不应该上全局 ​

这点很重要。

不是所有状态都该进 Vuex 或 Pinia。

更适合放在组件本地的通常包括:

  1. 一个输入框当前值
  2. 某个弹窗是否打开
  3. 某个局部折叠面板状态

更适合考虑共享状态管理的通常包括:

  1. 登录用户信息
  2. 权限信息
  3. 购物车
  4. 跨页面筛选条件
  5. 多组件共用的缓存数据

3. Vuex 是什么 ​

Vuex 可以看成 Vue2 时代最经典、最官方感的一套集中式状态管理方案。

它最核心的思想是:

  1. 共享状态集中放在 store
  2. 修改状态要走明确路径
  3. 状态流尽量保持可追踪

Vuex 里最常见的几个词是:

  1. state
  2. getters
  3. mutations
  4. actions
  5. modules

3.1 一段典型 Vuex 示例 ​

ts
const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 300)
    }
  }
})

这套设计的重点是:

  1. 同步修改通常走 mutation
  2. 异步逻辑通常放 action
  3. 派生值放 getter

4. Vuex 的优点和边界 ​

4.1 优点 ​

  1. 状态流清楚
  2. 组织方式规范
  3. 在中大型 Vue2 项目里非常常见

4.2 边界 ​

  1. 样板代码偏多
  2. 心智负担不小
  3. 小到中型项目里会显得有些重

5. Pinia 是什么 ​

Pinia 可以看成 Vue3 时代更轻、更现代、更贴近组合式 API 的状态管理方案。

它不是简单的“Vuex 改名版”,而是状态管理思路的一次收敛。

Pinia 更强调:

  1. 更少样板代码
  2. 更自然的类型推导
  3. 更贴近组合式 API 的使用方式

5.1 一段典型 Pinia 示例 ​

ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

在组件里使用时通常会像这样:

ts
const counterStore = useCounterStore()
counterStore.increment()

6. Pinia 为什么更受新项目欢迎 ​

核心原因通常不是“它更新”,而是:

  1. API 更直接
  2. 代码更少
  3. 类型推导更自然
  4. 和 Vue3 的组合式思路更一致

也就是说,Pinia 更适合现代 Vue 项目的默认心智模型。

7. Vuex 和 Pinia 的核心差异 ​

维度VuexPinia
常见时代背景Vue2 项目很常见Vue3 项目更常见
API 复杂度相对更重更轻
样板代码较多较少
类型体验一般更自然
与组合式 API 协调间接更顺

8. 选型时真正要看什么 ​

不要只按“新旧”二选一,更稳的判断是:

8.1 更适合 Vuex 的情况 ​

  1. 维护 Vue2 历史项目
  2. 现有团队和代码已经深度依赖 Vuex
  3. 短期没有迁移计划

8.2 更适合 Pinia 的情况 ​

  1. 新的 Vue3 项目
  2. 希望减少样板代码
  3. 对 TypeScript 体验更敏感
  4. 希望和组合式 API 更一致

9. 一个更贴近业务的 Pinia 示例 ​

ts
import { defineStore } from 'pinia'

type UserInfo = {
  id: number
  name: string
  role: string
}

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null as UserInfo | null,
    token: ''
  }),
  getters: {
    isAdmin: (state) => state.userInfo?.role === 'admin'
  },
  actions: {
    setUserInfo(userInfo: UserInfo) {
      this.userInfo = userInfo
    },
    clearLoginState() {
      this.userInfo = null
      this.token = ''
    }
  }
})

这个例子最值得先理解的是:

  1. 登录用户信息是典型共享状态
  2. 权限判断可以做成派生状态
  3. 清理登录态适合做成明确动作

10. Pinia 持久化怎么做 ​

在真实项目里,Pinia 很少只承担“页面打开期间的临时状态”。

很多状态会希望在刷新后继续保留,例如:

  1. 用户主题设置
  2. 登录后的部分会话信息
  3. 搜索筛选条件
  4. 表单草稿

Pinia 本身主要负责状态组织,不直接内置完整持久化方案。

工程里更常见的做法,是配合插件 pinia-plugin-persistedstate。

安装通常类似这样:

bash
pnpm add pinia-plugin-persistedstate

先在创建 Pinia 实例时注册插件:

ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

pinia.use(piniaPluginPersistedstate)

app.use(pinia)
app.mount('#app')

然后在具体 store 上声明哪些内容要持久化:

ts
import { defineStore } from 'pinia'

type UserInfo = {
  id: number
  name: string
  role: string
}

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null as UserInfo | null,
    token: '',
    theme: 'light',
  }),
  actions: {
    setLoginState(userInfo: UserInfo, token: string) {
      this.userInfo = userInfo
      this.token = token
    },
    setTheme(theme: 'light' | 'dark') {
      this.theme = theme
    },
    clearLoginState() {
      this.userInfo = null
      this.token = ''
    }
  },
  persist: {
    key: 'user-store',
    storage: localStorage,
    paths: ['userInfo', 'theme'],
  },
})

这段配置里最关键的是:

  1. persist: true 表示快速启用默认持久化
  2. persist: { ... } 表示做更细的自定义配置
  3. paths 用来限制只持久化哪些字段
  4. storage 决定底层写到哪里,常见是 localStorage 或 sessionStorage

如果你只想做最小配置,也可以直接写:

ts
persist: true

但工程上更常见的做法仍然是显式指定 paths,避免把整个 store 都落盘。

11. Pinia 持久化时该怎么判断边界 ​

不是所有 Pinia 状态都适合持久化。

更适合持久化的通常包括:

  1. 用户偏好设置
  2. 主题和语言
  3. 刷新后仍需保留的筛选条件
  4. 草稿信息

不太适合直接持久化的通常包括:

  1. 大体积列表结果
  2. 很容易过期的接口数据
  3. 只在当前页面临时存在的 UI 开关
  4. 高敏感信息

🌟 把这层分工记住:Pinia 负责状态建模,持久化插件负责“要不要把这部分状态保存到浏览器存储里”。

如果是登录态场景,更稳的工程习惯通常是:

  1. 持久化少量必要字段
  2. 不把过多敏感数据直接长期写入浏览器存储
  3. 应用启动后结合接口重新校验登录有效性

12. Vuex / Pinia 的持久化思路有什么共通点 ​

虽然 Vuex 和 Pinia API 不一样,但持久化思路其实很像:

  1. store 负责组织共享状态
  2. 持久化插件负责把部分状态同步到浏览器存储
  3. 页面初始化时再把已保存数据回灌回来

如果是历史 Vue2 项目,Vuex 里也常见配合持久化插件或手写订阅逻辑,把指定模块写进 localStorage。

但如果是 Vue3 新项目,工程上更常见的默认组合通常是:

  1. pinia
  2. pinia-plugin-persistedstate

13. Pinia / Vuex 和 @tanstack/vue-query 是什么关系 ​

如果只把前端状态分成“组件本地状态”和“全局共享状态”,很多工程问题还是解释不清。

因为还有一类状态更像:

  1. 接口返回的数据
  2. 请求后的缓存结果
  3. 何时重新拉取
  4. 数据失效后如何自动更新
  5. loading / error / success 这些请求生命周期状态

这类数据更贴近“服务端状态”,而不是普通客户端业务状态。

在 Vue 生态里,更常见的方案就是 @tanstack/vue-query。

安装通常类似这样:

bash
pnpm add @tanstack/vue-query

它更像:专门管理服务端状态获取、缓存、失效、重试和后台刷新的库。

所以把边界放回原位看:

工具更偏解决什么
Vuex / Pinia客户端共享业务状态、权限状态、界面协作状态
@tanstack/vue-query服务端数据拉取、缓存、重试、失效和刷新

例如下面这些数据,更适合优先考虑 vue-query:

  1. 商品列表
  2. 用户详情
  3. 评论列表
  4. 订单分页数据
  5. 搜索结果

而下面这些状态,通常仍然更适合 Pinia 或 Vuex:

  1. 主题模式
  2. 当前登录会话中的客户端展示信息
  3. 多组件共享的 UI 协作状态
  4. 复杂本地编辑状态

🌟 要注意的是,接口数据不是普通的“全局变量”,它天然带有缓存、失效时间和重取策略。

14. 一个最小 vue-query 接入示例 ​

先在应用入口注册 VueQueryPlugin:

ts
import { createApp } from 'vue'
import {
  QueryClient,
  VueQueryPlugin,
} from '@tanstack/vue-query'
import App from './App.vue'

const app = createApp(App)
const queryClient = new QueryClient()

app.use(VueQueryPlugin, {
  queryClient,
})

app.mount('#app')

然后在组件里使用 useQuery:

vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'

async function fetchUserList() {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('加载用户列表失败')
  }

  return response.json()
}

const { data, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUserList,
  staleTime: 60_000,
})
</script>

<template>
  <div v-if="isLoading">加载中...</div>
  <div v-else-if="error">加载失败</div>
  <ul v-else>
    <li v-for="user in data" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

这段示例最值得看懂的是:

  1. queryKey 是这份远程数据的身份标识
  2. queryFn 是真正的拉取逻辑
  3. staleTime 决定数据在多长时间内算“新鲜”

也就是说,组件不再自己重复维护一整套:

  1. loading
  2. error
  3. data
  4. 缓存复用
  5. 重新请求策略

15. Pinia 和 vue-query 怎么一起用 ​

现代 Vue3 项目里,很常见的组合其实是:

  1. Pinia 管客户端共享状态
  2. @tanstack/vue-query 管服务端状态

例如一个后台页面里:

  1. 列表接口、详情接口、分页数据用 vue-query
  2. 当前主题、侧边栏展开状态、复杂本地草稿状态用 Pinia

这样分工的好处是:

  1. Pinia 不需要硬塞大量接口缓存数据
  2. 接口数据天然拥有失效、缓存和重取能力
  3. 状态边界更清楚

如果是 Vue2 老项目,Vuex 仍然可以继续管理客户端共享状态;只是遇到服务端数据缓存问题时,也要优先思考“它到底是不是该放进 store”。

16. 状态管理最容易踩的坑 ​

16.1 什么都往全局状态里放 ​

最后会让 store 变成“全局大仓库”。

16.2 组件本地状态和共享状态边界不清 ​

会导致很多状态既分散又重复。

16.3 只关注“怎么改状态”,不关注“状态建模” ​

真正影响长期维护成本的,往往是状态结构本身。

16.4 状态拆分不按业务域,而按页面临时感受拆 ​

这样后面一旦跨页面复用,就会越来越乱。

17. 一份更稳的检查清单 ​

当你考虑要不要用 Vuex / Pinia 时,至少可以问:

  1. 这份状态是不是多个组件共享
  2. 这份状态会不会跨页面持续存在
  3. 这份状态有没有统一修改入口的价值
  4. 这个项目是 Vue2 还是 Vue3
  5. 当前团队更适合延续旧方案,还是切到更现代的方案

18. 小结 ​

如果把这篇压缩成一句话,可以记住:Vuex 和 Pinia 都是在解决共享状态治理问题,Vuex 更像 Vue2 时代的经典集中式方案,Pinia 更像 Vue3 时代更轻、更自然、更贴近组合式 API 的状态管理方案。

继续往下读,比较自然的下一步是进入 Vue Router:前端路由、导航守卫与页面组织。

基于 VitePress 构建的个人技术笔记。