跳转到内容

Pinia 基础

建议学习 45-60 分钟更新于 2026/5/20

Pinia 是 Vue 官方推荐的状态管理工具。

它的作用是把多个组件都需要访问的数据集中管理起来,让状态、派生状态和修改逻辑有一个清晰的位置。

如果只是一个组件内部自己使用的数据,直接放在组件里就可以。但如果数据需要在多个页面、多个组件之间共享,Pinia 就会变得很有用。

这篇笔记主要整理:

  • 为什么需要 Pinia。
  • Store 是什么。
  • 如何安装和注册 Pinia。
  • 如何定义和使用 Store。
  • stategettersactions 分别做什么。
  • 为什么解构 Store 时要使用 storeToRefs

在 Vue 项目中,组件之间共享数据是很常见的需求。

比如:

  • 导航栏要展示当前登录用户信息。
  • 多个页面都要读取购物车数量。
  • 权限信息需要在路由、按钮、页面中复用。
  • 多步骤表单切换页面后仍然要保留数据。

如果这些数据只靠父子组件传递,会越来越麻烦。组件层级一深,数据传递路径就会变长,维护成本也会变高。

Pinia 的价值就是把这些“跨组件共享的数据”放到统一的 Store 中。

它还能带来一些开发体验上的好处:

  • 支持 Vue Devtools 调试。
  • 可以查看状态变化和 action 调用。
  • 支持热更新,开发时修改 Store 不需要丢失已有状态。
  • 对 TypeScript 支持比较友好。
  • 支持插件扩展。
  • 支持服务端渲染场景。

简单说,Pinia 不是为了替代组件本地状态,而是用来管理真正需要共享的应用状态。

Store 可以理解为一个独立于组件树之外的状态容器。

它有点像一个一直存在的组件,任何需要它的组件都可以读取它,也可以通过它提供的方法修改状态。

Pinia 中 Store 主要包含三个概念:

Pinia 概念组件中的类比作用
statedata存放状态数据
getterscomputed根据状态计算派生数据
actionsmethods修改状态,处理同步或异步逻辑

比如一个计数器 Store:

  • count 是状态。
  • doubleCount 是根据 count 计算出来的派生值。
  • increment 是修改 count 的方法。

适合放进 Pinia 的数据通常有一个特点:多个地方都要用,或者需要跨页面保留。

适合放进 Store:

  • 用户信息。
  • 登录状态。
  • 权限信息。
  • 购物车数据。
  • 多页面共享的筛选条件。
  • 复杂多步骤表单数据。

不太适合放进 Store:

  • 某个弹窗是否打开。
  • 某个按钮是否 loading。
  • 某个局部输入框的临时值。
  • 只在当前组件内部使用的数据。

判断标准可以简单一点:如果这个数据只属于当前组件,就放在当前组件里;如果多个组件都需要它,再考虑放进 Pinia。

使用 npm:

Terminal window
npm install pinia

使用 yarn:

Terminal window
yarn add pinia

使用 pnpm:

Terminal window
pnpm add pinia

安装完成后,需要在入口文件中创建 Pinia 实例,并通过 app.use() 注册到 Vue 应用中。

main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')

注册完成后,组件中就可以使用 Store 了。

Pinia 使用 defineStore() 定义 Store。

defineStore() 的第一个参数是 Store 的唯一 ID,第二个参数可以是对象,也可以是一个函数。

Option Store 和 Vue Options API 的风格比较接近。

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

这种写法很直观:

  • state 返回状态对象。
  • getters 定义计算属性。
  • actions 定义修改状态的方法。

actions 中可以通过 this 访问当前 Store。

Setup Store 更接近 Vue Composition API。

stores/counter.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubleCount,
increment,
}
})

在 Setup Store 中:

  • ref() 定义的数据会变成 state
  • computed() 定义的数据会变成 getters
  • 普通函数会变成 actions

如果项目主要使用 <script setup> 和 Composition API,Setup Store 会更自然。

需要注意:Store 中希望暴露给外部使用的状态、计算属性和方法,都要在最后 return 出去。

定义好 Store 后,在组件中导入并执行对应的 use...Store() 函数即可。

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<button @click="counterStore.increment">
{{ counterStore.count }}
</button>
<p>双倍计数:{{ counterStore.doubleCount }}</p>
</template>

这里有两个细节:

  • useCounterStore() 执行后会得到 Store 实例。
  • 在模板中读取 Store 状态时,不需要写 .value

Pinia 会帮你处理响应式访问。

下面把计数、计算属性、异步请求放在同一个 Store 中。

stores/user.ts
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
type User = {
id: number
name: string
role: string
}
const API_URL = 'http://127.0.0.1:9090/list'
export const useUserStore = defineStore('user', () => {
const count = ref(0)
const users = ref<User[]>([])
const loading = ref(false)
const doubleCount = computed(() => count.value * 2)
const userCount = computed(() => users.value.length)
function increment() {
count.value++
}
async function getUsers() {
loading.value = true
try {
const response = await axios.get<User[]>(API_URL)
users.value = response.data
} finally {
loading.value = false
}
}
return {
count,
users,
loading,
doubleCount,
userCount,
increment,
getUsers,
}
})

这个 Store 中:

  • countusersloading 是状态。
  • doubleCountuserCount 是 getter。
  • incrementgetUsers 是 action。
  • getUsers 是异步 action,可以请求接口并更新状态。
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
onMounted(() => {
userStore.getUsers()
})
</script>
<template>
<button @click="userStore.increment">
计数:{{ userStore.count }}
</button>
<p>双倍计数:{{ userStore.doubleCount }}</p>
<p>用户数量:{{ userStore.userCount }}</p>
<p v-if="userStore.loading">加载中...</p>
<table v-else>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>角色</th>
</tr>
</thead>
<tbody>
<tr v-for="item in userStore.users" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.role }}</td>
</tr>
</tbody>
</table>
</template>

组件只关心“什么时候调用 action”和“如何展示状态”,真正的数据请求和状态修改放在 Store 中。

这样组件会更干净,业务逻辑也更容易复用。

Pinia 不需要像 Vuex 那样区分 mutationaction

在 Pinia 中,actions 既可以写同步逻辑,也可以写异步逻辑。

function increment() {
count.value++
}
async function fetchList() {
const response = await axios.get('/api/list')
list.value = response.data
}

这也是 Pinia 比 Vuex 更轻的一点:概念更少,写法更直接。

在 Setup Store 中,getter 通常使用 computed() 实现。

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

在组件中使用时:

<template>
<p>{{ counterStore.doubleCount }}</p>
</template>

模板里不需要写 doubleCount.value

使用 Store 时,一个常见问题是解构导致响应式丢失。

下面这种写法不推荐:

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = counterStore
</script>

原因是 Store 本身是响应式对象,直接解构普通属性会破坏响应式连接。

如果确实想解构状态和 getter,应该使用 storeToRefs()

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore
</script>
<template>
<button @click="increment">
{{ count }}
</button>
<p>{{ doubleCount }}</p>
</template>

这里的规则很重要:

  • 状态和 getter 使用 storeToRefs() 解构。
  • action 可以直接从 Store 中解构。

因为 action 本身已经和 Store 绑定,不需要转换成 ref。

并不是所有场景都必须解构。

如果模板里使用的状态不多,直接使用 Store 反而更清晰:

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<button @click="counterStore.increment">
{{ counterStore.count }}
</button>
</template>

这也是我更推荐的入门写法。

等页面里用到的字段变多,再考虑用 storeToRefs() 做解构。

Pinia 很方便,但不要把所有状态都塞进 Store。

例如一个列表页里:

  • 当前用户信息:适合放 Store。
  • 当前页面的搜索关键词:看是否需要跨页面保留。
  • 一个筛选弹窗是否打开:通常放组件本地。
  • 表格当前展开哪一行:通常放组件本地。

Store 应该服务于共享状态,而不是把组件内部的临时交互全部搬出去。

如果 Store 变成所有状态的杂货箱,后期维护会很难。

Pinia 是 Vue 的集中状态管理工具,常用于跨组件、跨页面共享状态。

可以把它理解为新一代 Vuex,但写法更轻,类型推导更好,也不再需要手写 mutation。

不需要。

Pinia 中通过 actions 修改状态。actions 同时支持同步和异步逻辑。

如果使用 Setup Store,通常使用 computed()

如果使用 Option Store,则写在 getters 选项中。

Pinia 的 Store 解构后如何保持响应式

Section titled “Pinia 的 Store 解构后如何保持响应式”

状态和 getter 使用 storeToRefs()

action 可以直接解构。

const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore

一般放在 src/stores 目录下,并按业务模块拆分。

例如:

src/
stores/
user.ts
cart.ts
counter.ts

命名上通常使用 useXxxStore

export const useUserStore = defineStore('user', () => {})
export const useCartStore = defineStore('cart', () => {})

这样和 Vue 组合式函数的命名习惯保持一致。

Pinia 的核心并不复杂。

入门时先记住四句话就够了:

  • defineStore() 用来定义 Store。
  • ref() 是状态,computed() 是 getter,函数是 action。
  • 组件中执行 useXxxStore() 得到 Store 实例。
  • 解构状态和 getter 时使用 storeToRefs()

真正写项目时,最重要的是判断哪些数据值得进入 Store。只要这个边界判断清楚,Pinia 会让 Vue 项目的状态管理轻很多。