状态管理的本质是决定数据的“居住地”:它该放在组件内部,还是提升到一个全局 Store 里?这个选择直接影响代码的可维护性和复杂度。Vue 的灵活性意味着两者可以共存,关键是根据数据的使用方式做出判断。
两条金的择规则
1. 就近原则:状态离谁近,就放谁那儿
如果一个数据只被单个组件(或及其直系子组件)使用,那它就应该住在组件自己的 setup 里,用 ref 或 reactive 管理。这不仅是代码最小化的实践,也让数据的生命周期与组件绑定——组件销毁时,状态自动释放,无需手动清理。
<script setup>
// 组件本地状态:表单输入框的临时内容
const searchKeyword = ref('')
</script>
2. 共享原则:状态需要跨组件共享时,才考虑提升
当一个数据需要被多个不相关的组件读取或修改,或者需要在跨路由/页面间保持时,才将其移动到 Pinia Store 中。这样避免了通过层层 props 传递(prop drilling)或事件总线通信带来的混乱。
典型场景对照表
| 场景 | 使用组件本地状态(ref/reactive) | 使用 Pinia Store |
|------|----------------------------------|------------------|
| 表单的输入值、校验状态 | ✅ 提交前只在表单组件内流转 | ❌ 除非表单是全局多步骤且需跨页面保持 |
| 模态框的显示/隐藏 | ✅ 一般只在当前页面触发 | ❌ 除非是全局通知中心,多处会触发同一模态框 |
| 用户登录信息(昵称、权限) | ❌ 导航栏、用户中心等大量组件需要 | ✅ 全局 Store,登录后更新,退出后清除 |
| 购物车数据 | ❌ 商品详情、购物车页、结算页共享 | ✅ Store + 持久化插件,跨路由保持 |
| 当前路由参数 ($route) | ❌ Vue Router 内置状态,直接用 useRoute() | ❌ 不需要 Pinia,路由已是全局 |
| 组件内部的计算结果 | ✅ computed 就地计算 | ❌ 除非计算结果被多个页面共享(罕见) |
| 接口缓存数据(如用户列表) | ✅ 若仅本页面使用 | ✅ / 🤷 跨页面复用时可用 Pinia,更推荐 Vue Query 管理服务端数据缓存 |
决策四问
在写一个新功能时,对着数据问自己这四个问题:
- 这个数据只在本组件内用吗? → 是 →
ref/reactive - 这个数据会被兄弟组件或完全无关的组件访问吗? → 是 → 考虑 Pinia
- 这个数据需要在路由跳转(页面切换)后仍然保留吗? → 是 → 必须 Pinia(或配合持久化)
- 这个数据本质上是后端数据的“前端快照”吗? → 是 → 优先考虑 Vue Query 管理缓存,Pinia 只用来存储少量 UI 状态(如当前选中项)
警惕两种极端
- 全局 Store 泛滥:把所有状态不分青红皂白塞进 Pinia,导致 Store 臃肿、数据流难以追踪,组件与 Store 强耦合,测试困难。记住:不是所有状态都值得全局化。
- Props 地狱:为了“保持组件纯净”而拒绝使用 Store,导致数据通过层层 props 传递五六个层级,中间组件被迫接收与己无关的数据,修改时又需要层层 emits 回传。这种时候引入 Pinia 是明智的。
真正的平衡
一个健康的 Vue 项目中,往往 80% 的状态活在组件内部,20% 的状态放在 Pinia Store 中。这 20% 是应用真正的“全局血液”,其余的都是局部临时的“细胞活动”。不要因为 Pinia 好用就处处用,也不要因为崇尚简单而困在 prop drilling 里。根据数据的使用边界做出判断,才是状态管理选型的成熟思路。