Pinia 状态管理实战:定义 Store、修改数据、模块拆分

一、Pinia 安装与注册

Pinia 是 Vue3 官方推荐的状态管理库,取代 Vuex:API 更简洁、天然支持 TS、无 mutations。

bash 复制代码
npm install pinia
ts 复制代码
// 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");

与 Vuex 对比

对比项 Vuex Pinia
mutations 必须有(同步改 state) 没有,直接改 state / actions
TypeScript 类型推导弱 类型友好
模块 modules 嵌套 每个 store 独立定义
体积 较大 更轻(仅 ~1KB core)

二、defineStore 定义仓库

两种风格:选项式 (对象)与 setup 风格(函数),推荐 setup 风格。

2.1 选项式风格

ts 复制代码
// stores/user.ts
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  state: () => ({
    name: "",
    token: localStorage.getItem("token") || "",
  }),
  getters: {
    isLogin: (state) => !!state.token,       // 派生状态
  },
  actions: {
    setToken(token: string) {
      this.token = token;
    },
  },
});

2.2 setup 风格(推荐)

ts 复制代码
// stores/counter.ts
import { ref, computed } from "vue";
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
  // state
  const count = ref(0);
  const double = ref(2);

  // getters
  const doubleCount = computed(() => count.value * 2);

  // actions
  function increment() {
    count.value++;
  }

  return { count, double, doubleCount, increment };
});
构成 选项式写法 setup 风格写法
state state: () => ({...}) ref() / reactive()
getters getters: { xx(state) {...} } computed()
actions actions: { xx() {...} } 普通函数

三、组件中读取与修改 store

3.1 基础读写

vue 复制代码
<script setup lang="ts">
import { useCounterStore } from "@/stores/counter";

const store = useCounterStore();

store.count++;            // 直接修改 state(无需 mutations)
store.increment();        // 调用 action
</script>

<template>
  <p>{{ store.count }} × 2 = {{ store.doubleCount }}</p>
  <button @click="store.increment()">+1</button>
</template>

3.2 $patch 批量修改

ts 复制代码
store.$patch({
  count: store.count + 1,
  double: store.count * 2,
});

// 函数式写法:可写逻辑
store.$patch((state) => {
  state.count++;
  state.double = state.count * 2;
});

3.3 storeToRefs 保持响应式

ts 复制代码
import { storeToRefs } from "pinia";
import { useCounterStore } from "@/stores/counter";

const store = useCounterStore();
const { count, doubleCount } = storeToRefs(store);   // 解构后仍是响应式
const { increment } = store;                          // action 直接解构

四、store 之间互相调用

ts 复制代码
// stores/order.ts
import { defineStore } from "pinia";
import { useUserStore } from "./user";

export const useOrderStore = defineStore("order", () => {
  const userStore = useUserStore();      // 在另一个 store 内直接使用

  const orders = ref<number[]>([]);

  function fetchOrders() {
    // 读取其他 store 的状态
    if (!userStore.isLogin) return;
    orders.value = [1, 2, 3];
  }

  return { orders, fetchOrders };
});

五、异步请求写在 actions

ts 复制代码
// stores/article.ts
import { ref } from "vue";
import { defineStore } from "pinia";
import api from "@/api";           // 封装好的请求

export const useArticleStore = defineStore("article", () => {
  const list = ref<Article[]>([]);
  const loading = ref(false);

  async function fetchList(params: Query) {
    loading.value = true;
    try {
      list.value = await api.getArticleList(params);   // actions 支持 async/await
    } finally {
      loading.value = false;
    }
  }

  return { list, loading, fetchList };
});
vue 复制代码
<!-- 组件中 -->
<script setup lang="ts">
import { onMounted } from "vue";
import { useArticleStore } from "@/stores/article";
import { storeToRefs } from "pinia";

const store = useArticleStore();
const { list, loading } = storeToRefs(store);

onMounted(() => store.fetchList({ page: 1 }));
</script>

六、踩坑:解构丢失响应式

坑 现象 解决
直接解构 state const { count } = store 后页面不更新 用 storeToRefs(store)
解构 getters 同上 storeToRefs
直接解构 actions 丢失 this 上下文报错(选项式) action 直接 const { fn } = store(setup 风格无此问题)
非响应式坑 store 中存了普通对象(非 ref)被替换 用 ref/reactive 包裹
$reset 不生效 setup 风格 store 无内置 $reset 手写 reset 函数返回初始值
组件外使用 store 在普通工具函数中调用报错(未装 pinia) 确保在 app.use(pinia) 后调用
ts 复制代码
// 正确解构姿势
const { count, doubleCount } = storeToRefs(store);   // state/getters
const { increment } = store;                          // actions

七、总结

  • 定位:Vue3 官方状态管理,无 mutations、TS 友好,取代 Vuex;
  • 定义 :defineStore 选项式或 setup 风格,state/getters/actions 三件套;
  • 读写 :直接改 state、$patch 批量、actions 承载异步;
  • 跨 store :store 内直接 useXxxStore() 互相调用;
  • 异步:actions 用 async/await 写请求,loading 状态一并管理;
  • 踩坑 :解构必须 storeToRefs,actions 直接解构,setup 风格 $reset 需手写。

本系列前篇:《Vue3 零基础上手》《Vue3 组合式 API 深度解析》《Vue3 组件开发》《Vue Router4 完整指南》;后续:《Vue3 项目工程实战》。

相关推荐
SGAMERrain几秒前
# Nuxt 4 + MediaPipe:如何在浏览器中实现本地 AI 人脸分析?
前端
github_is_geeksKai1 小时前
浏览器端文件处理的边界:PDF 面单、GPX 隐私与 EPUB 修复
前端
deli0072 小时前
从一句反馈到一次升级:Mystudy 的自进化实践
前端
FungLeo2 小时前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装
FungLeo4 小时前
成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界
前端·javascript·vue.js·rbac·vue router·路由守卫·前端权限
FungLeo4 小时前
成为全栈·Vue 管理后台篇·分页只显示第一页:从接口total 定位到前端修复
前端·vue.js·状态模式·分页·naive ui·服务端分页·前端问题排查
ZhengEnCi5 小时前
苹果滑动为什么有「肉感」:橡皮筋越界阻尼与弹簧动画的物理原理
前端
Moment5 小时前
不会后期的我,发现了一个开源胶片神器😍😍😍
前端·后端·开源
L1mbo5 小时前
Lite模式 缓解微信开发者工具卡顿,附 uni-app 实践方案
前端·微信小程序
FungLeo5 小时前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query