Pinia入门指南:三步上手,搞定状态管理

大家好,我是加洛斯。是一名全栈工程师👨‍💻,这里是我的知识笔记与分享,旨在把复杂的东西讲明白。如果发现有误🔍,万分欢迎你帮我指出来!废话不多说,正文开始 👇

一、什么是Pinia

PiniaVue.js的下一代状态管理库 ,由Vue.js核心团队维护。相比Vuex``Pinia提供了更简洁、直观的API

"状态"就是当前程序运行时"留在内存里的、会变化的数据"

状态管理就是把这些数据从"散落在各个组件里"抽出来,集中存、集中改、集中通知 ,让任何组件都能同一份数据源、统一规则地读取和更新,避免:

  • 父→子→孙,一层层 props 传递(props drilling)
  • 事件一层层 $emit 回去
  • 不同组件各自 copy 一份数据,改完互相不一致

二、上手Pinia

2.1 安装Pinia

js 复制代码
# 使用npm
npm install pinia

# 使用yarn
yarn add pinia

# 使用pnpm
pnpm add pinia

2.2 初始化与基础配置

在Vue应用中初始化Pinia:

javascript 复制代码
// main.js / main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

// 创建Pinia实例
const pinia = createPinia()

// 创建Vue应用并挂载Pinia
const app = createApp(App)
app.use(pinia)
app.mount('#app')

三、创建Stroe

3.1 什么是 Store?

StorePinia的核心概念,它是一个保存状态业务逻辑 的容器。可以把Store理解为:

  • 数据仓库:存储应用程序的状态
  • 业务逻辑中心:处理数据的获取、修改
  • 可复用模块:可以在任何组件中使用

3.2 创建基础 Store

在项目中创建stores目录,然后创建一个Store,我们先来一个简单的案例,就弄一个计数器:counterStore

javascript 复制代码
import { defineStore } from 'pinia'

// 使用 defineStore 定义 Store
// 第一个参数 'counter' 是 Store 的唯一ID,方便其他组件使用这个 Store
// 第二个参数是 Store 的配置
export const counterStore = defineStore('counter', {
  // state: 定义初始状态(相当于组件的data)
  state: () => ({
    count: 1,
    name: '我的计数器'
  }),
  // actions: 方法(相当于组件的方法)
  actions: {
    increment() {
      this.count++  // 使用 this 访问 state
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 1
    },
    setName(newName) {
      this.name = newName
    }
  }
})

3.3 在组件中使用Store

这个我们一步一步来,先给各位讲清楚

  1. 我们首先引用刚刚创建的counterStore,引用后我们获取这个组件的实例,接下来我们就可以使用这个stroe了。
js 复制代码
import { counterStore } from '@/stores/counterStore'

// 使用Store
const store = counterStore()
  1. 我们先在页面显示初始化的内容
js 复制代码
<template>
  <div class="counter">
    <h2>{{ store.name }}</h2>
    <!-- 显示状态 -->
    <p>当前计数:{{ store.count }}</p>
  </div>
</template>

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

// 使用Store
const store = counterStore()
</script>

可以看到我们刚刚自定义的name与count已经成功被我们获取到了!

  1. 接下来我们可以调用组件中的一些方法,可以直接使用 实例.方法名 来调用方法,这些方法都是我们在counterStore中定义的。
js 复制代码
<template>
  <div class="counter">
    <h2>{{ store.name }}</h2>
    <!-- 显示状态 -->
    <p>当前计数:{{ store.count }}</p>
    <!-- 调用action -->
    <div class="buttons" style="display: flex; gap: 5px">
      <button @click="store.increment()">增加</button>
      <button @click="store.decrement()">减少</button>
      <button @click="store.reset()">重置</button>
    </div>
  </div>
</template>

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

// 使用Store
const store = counterStore()
</script>

至此,一个简单的store就写好了。

3.4 数据持久化

Piniastate只是运行时内存里的普通 JS 对象;一旦 F5/刷新,整个 JS 上下文被销毁,内存被清空,状态自然跟着消失;

Pinia 本身不负责持久化 ,它只做"状态管理",而不是"状态存档"。上面的例子我们可以进行刷新,然后当前计数就会重新变为1,但有的时候我们需要保持我们的状态,例如你登录时候存储的信息,不能一刷新就没了,所以我们需要用到pinia-plugin-persistedstate插件。

  1. 首先安装pinia-plugin-persistedstate插件
js 复制代码
npm i pinia-plugin-persistedstate
  1. 接下来在main.js/main.ts中引用
js 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
  1. Store中添加persist: true开启数据持久化
js 复制代码
import { defineStore } from 'pinia'

// 使用 defineStore 定义 Store
// 第一个参数 'counter' 是 Store 的唯一ID,方便其他组件使用这个 Store
// 第二个参数是 Store 的配置
export const counterStore = defineStore('counter', {
  // 开启数据持久化
  persist: true,
  // state: 定义初始状态(相当于组件的data)
  state: () => ({
    count: 1,
    name: '我的计数器'
  }),
  // actions: 方法(相当于组件的方法)
  actions: {
    increment() {
      this.count++  // 使用 this 访问 state
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 1
    },
    setName(newName) {
      this.name = newName
    }
  }
})

这样操作之后即使是刷新页面,计数器也会保持刚刚你所操作的结果。

四、State 详解

Pinia里,state 就是 "一段会触发界面自动刷新的普通 JavaScript 数据"

  1. 它本质上是一个被 Vue 包装过的对象/基本类型 (底层用 reactive() / ref() 做的代理)。
  2. 你对它做任何修改,Vue 都能感知到 ,然后自动把用到这份数据的组件重新渲染
  3. 在 Pinia 的 defineStore 里,state 通常写成函数返回对象,用来给每个Store实例提供独立的初始数据

4.1 定义各种类型的State

js 复制代码
// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    // 基本类型
    id: null,
    username: '',
    isLoggedIn: false,
    age: 0,
    
    // 对象类型
    profile: {
      avatar: 'default-avatar.png',
      email: '',
      phone: '',
      address: {
        city: '',
        district: '',
        detail: ''
      }
    },
    
    // 数组类型
    roles: ['user'],  // 默认角色
    permissions: [],
    favoriteIds: [1, 3, 5],
  }),
})

4.2 响应式解构的正确方式

js 复制代码
<template>
  <div>
    <!-- ❌ 错误做法 -->
    <div>
      <h3>❌ 错误:直接解构</h3>
      <p>用户名:{{ username }}</p>
      <p>主题:{{ theme }}</p>
      <input v-model="username" placeholder="修改用户名试试">
      <p>修改后视图不会更新!</p>
    </div>
    
    <!-- ✅ 正确做法 -->
    <div>
      <h3>✅ 正确:使用storeToRefs</h3>
      <p>用户名:{{ reactiveUsername }}</p>
      <p>主题:{{ reactiveTheme }}</p>
      <input v-model="reactiveUsername" placeholder="修改用户名">
      <p>视图会实时更新!</p>
    </div>
    
    <!-- ✅ 另一种正确做法 -->
    <div>
      <h3>✅ 正确:直接访问Store</h3>
      <p>用户名:{{ userStore.username }}</p>
      <p>年龄:{{ userStore.age }}</p>
      <input v-model="userStore.username" placeholder="直接修改">
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// ❌ 错误:直接解构(失去响应性)
const { username, settings } = userStore
const theme = settings.theme

// ✅ 正确:使用storeToRefs
const { 
  username: reactiveUsername, 
  age: reactiveAge,
  settings: reactiveSettings 
} = storeToRefs(userStore)
const reactiveTheme = reactiveSettings.value.theme
</script>

4.3 解构赋值storeToRefs

storeToRefs 是 Pinia 官方提供的一个小工具函数 ,只做一件事儿: "把 Store 里的所有 state(包括嵌套对象)一次性转成 ref,同时保持响应性。"

js 复制代码
<template>
  <div style="display: flex; gap: 40px">
    <!-- ❌ 错误:直接解构 -->
    <div>
      <h3>❌ 错误:直接解构(丢失响应性)</h3>
      <p>count:{{ count }}</p>
      <p>name:{{ name }}</p>
      <input v-model="name" placeholder="改名字" />
      <button @click="increment">+1</button>
      <p>点按钮 / 改输入框 → 视图<strong>不会</strong>更新</p>
    </div>

    <!-- ✅ 正确:storeToRefs -->
    <div>
      <h3>✅ 正确:storeToRefs</h3>
      <p>count:{{ reactiveCount }}</p>
      <p>name:{{ reactiveName }}</p>
      <input v-model="reactiveName" placeholder="改名字" />
      <button @click="inc">+1</button>
      <p>操作 → 视图<strong>会</strong>实时更新</p>
    </div>

    <!-- ✅ 直接访问 store -->
    <div>
      <h3>✅ 直接访问 store</h3>
      <p>count:{{ store.count }}</p>
      <p>name:{{ store.name }}</p>
      <input v-model="store.name" placeholder="直接改" />
      <button @click="store.increment">+1</button>
    </div>
  </div>
</template>

<script setup>
import { counterStore } from '@/stores/counterStore'
import { storeToRefs } from 'pinia'

const store = counterStore()

/* ❌ 直接解构 → 失去响应性 */
const { count, name, increment } = store

/* ✅ 保持响应性 */
const { count: reactiveCount, name: reactiveName } = storeToRefs(store)
function inc() {
  store.increment()
} // 方法不用解构,直接调
</script>

五、Getters

GettersPinia中用于计算派生状态的函数,类似于Vue中的计算属性。它们用于从store的状态中派生出新的值,并且会缓存结果,只有当依赖的状态发生变化时才会重新计算。

5.1 基本用法

js 复制代码
import { defineStore } from 'pinia'

// 使用 defineStore 定义 Store
// 第一个参数 'counter' 是 Store 的唯一ID,方便其他组件使用这个 Store
// 第二个参数是 Store 的配置
export const counterStore = defineStore('counter', {
  // 开启数据持久化
  persist: true,
  // state: 定义初始状态(相当于组件的data)
  state: () => ({
    count: 1,
    name: '我的计数器'
  }),
  // getters: 计算属性(相当于组件的computed)
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },
  // actions: 方法(相当于组件的方法)
  actions: {
    increment() {
      this.count++  // 使用 this 访问 state
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 1
    },
    setName(newName) {
      this.name = newName
    }
  }
})
js 复制代码
<template>
  <div class="counter">
    <h2>{{ store.name }}</h2>
    <!-- 显示状态 -->
    <p>当前计数:{{ store.count }}</p>
    <p>当前计数的两倍:{{ store.doubleCount }}</p>
    <!-- 调用action -->
    <div class="buttons" style="display: flex; gap: 5px">
      <button @click="store.increment()">增加</button>
      <button @click="store.decrement()">减少</button>
      <button @click="store.reset()">重置</button>
    </div>
  </div>
</template>

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

// 使用Store
const store = counterStore()
</script>

我们来看结果,是不是很简单!

5.2 高级特性

  1. 访问其他 Store 的 Getters
javascript 复制代码
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'

export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null
  }),
  
  getters: {
    // 访问其他 store
    isAdmin() {
      const authStore = useAuthStore()
      return authStore.user?.role === 'admin'
    },
    
    // 结合本 store 状态
    userInfo(state) {
      const authStore = useAuthStore()
      return {
        ...state.profile,
        role: authStore.user?.role
      }
    }
  }
})
  1. TypeScript 支持
typescript 复制代码
import { defineStore } from 'pinia'

interface Item {
  id: number
  name: string
  price: number
}

interface CounterState {
  count: number
  items: Item[]
}

export const useCounterStore = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    items: []
  }),
  
  getters: {
    // 明确指定返回类型
    doubleCount(state): number {
      return state.count * 2
    },
    
    // 带参数的 getter
    getItemById(state): (id: number) => Item | undefined {
      return (id: number) => state.items.find(item => item.id === id)
    },
    
    // 使用 this 的类型推断
    totalPrice(): number {
      return this.items.reduce((total, item) => total + item.price, 0)
    }
  }
})
  1. 组合式 API 风格(Setup Store)
javascript 复制代码
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const items = ref([
    { id: 1, name: 'Item A', price: 10 },
    { id: 2, name: 'Item B', price: 20 }
  ])
  
  // Getters 使用 computed 定义
  const doubleCount = computed(() => count.value * 2)
  const totalPrice = computed(() => 
    items.value.reduce((total, item) => total + item.price, 0)
  )
  
  // 带参数的 getter(使用函数返回 computed)
  const getItemById = (id) => 
    computed(() => items.value.find(item => item.id === id))
  
  return {
    count,
    items,
    doubleCount,
    totalPrice,
    getItemById
  }
})

5.3 避免的常见问题

javascript 复制代码
getters: {
  // ❌ 错误:直接修改状态
  incrementCount(state) {
    state.count++  // 不要在 getter 中修改状态!
  },
  
  // ❌ 错误:没有使用 state 参数
  doubleCount() {
    return this.count * 2  // 在选项式 API 中应该使用 state 参数
  },
  
  // ✅ 正确
  doubleCount(state) {
    return state.count * 2
  },
  
  // ✅ 正确(setup 风格中使用 this)
  doubleCount() {
    return this.count * 2  // 在 setup 风格中可以使用 this
  }
}
相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax