Pinia 4.1.0:六个不报错、只是不生效的地方
Pinia 4.1.0 与 @pinia/testing 2.0.2 于 2026-10-14 发布。仓库在 vuejs/pinia,完整变更见 releases。下面几处问题的共同点是:控制台没有报错,状态看上去也写进去了,只是依赖没有更新,或者清理得不对。
1. 对象语法 $patch 不触发 shallowRef
store 里把大数组或第三方实例包成 shallowRef 是常见做法,为了避免深层代理。问题出在用对象语法 patch 时:值写进去了,依赖却没有被通知。
export const useListStore = defineStore('list', () => {
const items = shallowRef([])
return { items }
})
// 4.1.0 之前:不触发依赖更新
store.$patch({ items: ['a', 'b'] })
4.1.0 修复了这条路径(#2861、#3177)。在旧版本上,或者不想等升级时,有两种写法可以绕开:
// 直接赋值,走 shallowRef 的 setter
store.items = ['a', 'b']
// 函数式 patch,赋值经过响应式对象
store.$patch((state) => {
state.items = ['a', 'b']
})
2. dispose 已被替换的 store 会告警
同一 id 的 store 被新实例替换后,旧引用可能还留在某个闭包里。这时再对旧实例调用 $dispose() 属于误用,此前是静默的,4.1.0 起会给出警告(#3193)。
const store = useUserStore()
// …该 store 被新实例替换
store.$dispose() // 4.1.0:控制台出现警告
要清理就针对当前实例,不要拿旧引用做收尾。
3. getStores() 批量检查与清理
新增的 getStores()(#3194)返回当前 Pinia 实例里注册的 store 集合,适合测试 teardown 和 SSR 请求结束时统一释放。
import { getStores } from 'pinia'
getStores().forEach((store) => store.$dispose())
配合上一条,也不必再自己维护一张「已创建 store」的登记表。
4. HMR 保留空占位对象的运行时状态
setup store 里先用 ref({}) 占位、运行时再往上挂字段,热更新后这些字段以前会丢。4.1.0 保留了空对象、空集合这类占位对象的运行时状态(#2931、#3175)。
export const useConfigStore = defineStore('config', () => {
const config = ref>({})
return { config }
})
// 运行时
useConfigStore().config.theme = 'dark'
// 4.1.0 起,HMR 之后 theme 仍在
5. @pinia/testing 2.0.2:SSR 水合改为替换
reactive 的 Set/Map 状态在 hydration 时,旧行为是和服务端状态做并集,测试里会莫名多出元素。2.0.2 起直接用服务端状态替换(#3162)。
// 客户端水合前已有
store.selected = new Set(['b'])
// 服务端状态为 new Set(['a'])
// 2.0.2 之前:并集 -> Set { 'a', 'b' }
// 2.0.2 起:替换 -> Set { 'a' }
断言要跟着改成替换语义,否则测试仍然通过,但它验证的并不是真实的水合结果。
6. Pinia 4 本体:ESM only 与 storeToRefs
Pinia 4 只包含技术性破坏变更:ESM only,以及 @vue/devtools-api 升到 v8 后必须和 pinia 一起安装。
npm i pinia @vue/devtools-api
storeToRefs 现在会跳过 nullish 值(#3134),并对非法属性在开发期给出诊断;插件的错误值、用 Nostics 重构后的错误与警告信息也更具体。同批还修了 shouldHydrate 对非普通对象上 skipHydrate 标记的识别、同一回调重复订阅导致的重复 watcher,以及 devtools 里可写 getter(computed)的编辑。
迁移检查清单
- 构建目标切到 ESM,确认没有残留的 CJS require 入口
- 显式安装
@vue/devtools-api@8 - 搜一遍所有
$patch({ ... }),确认目标字段不是 shallowRef;是的话改成直接赋值或函数式写法 - 检查 teardown 里对旧 store 引用的
$dispose(),改用getStores()统一清理 - 用手写的实例登记表换成
getStores() - HMR 相关逻辑不再依赖「重新挂字段」的旧行为
- @pinia/testing 升到 2.0.2,SSR 测试中 Set/Map 断言改为替换语义
storeToRefs解构处允许 nullish 值,去掉为它加的兜底 watch- class 实例等非普通对象上的 skipHydrate 仍按预期跳过
- 同一个 watcher 回调只订阅一次