Skip to content

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

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

这不算错,但太浅。

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

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

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

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

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

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

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

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

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

这点很重要。

不是所有状态都该进 VuexPinia

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

  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 决定底层写到哪里,常见是 localStoragesessionStorage

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

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. 搜索结果

而下面这些状态,通常仍然更适合 PiniaVuex

  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 构建的个人技术笔记。