id: 'main',
state: () => ({
n: 0,
+ arr: [],
+ nestedArr: {
+ arr: [],
+ },
+ nested: {
+ a: 'a',
+ },
}),
actions: {
expect(store.other).toBe('new')
})
- it('adds new state properties', () => {
+ it('patches nested objects', () => {
const useStore = defineStore(baseOptions)
const store: any = useStore()
- store.n++
// simulate a hmr
defineStore({
...baseOptions,
- state: () => ({ newOne: 'hey', n: 0 }),
+ state: () => ({ nested: { a: 'b', b: 'b' } }),
})(null, store)
- expect(store.$state).toEqual({ n: 1, newOne: 'hey' })
- expect(store.n).toBe(1)
- expect(store.newOne).toBe('hey')
+ expect(store.$state).toEqual({ nested: { a: 'a', b: 'b' } })
defineStore({
...baseOptions,
- state: () => ({ other: 'new', n: 0 }),
+ state: () => ({ nested: { b: 'c' } }),
+ })(null, store)
+ // removes the nested a
+ expect(store.$state).toEqual({ nested: { b: 'b' } })
+ })
+
+ it('skips arrays', () => {
+ const useStore = defineStore(baseOptions)
+ const store: any = useStore()
+
+ // simulate a hmr
+ defineStore({
+ ...baseOptions,
+ state: () => ({ arr: [2] }),
+ })(null, store)
+
+ expect(store.$state).toEqual({ arr: [] })
+
+ defineStore({
+ ...baseOptions,
+ state: () => ({ arr: [1] }),
+ })(null, store)
+ expect(store.$state).toEqual({ arr: [] })
+ })
+
+ it('skips nested arrays', () => {
+ const useStore = defineStore(baseOptions)
+ const store: any = useStore()
+
+ // simulate a hmr
+ defineStore({
+ ...baseOptions,
+ state: () => ({ nestedArr: { arr: [2] } }),
+ })(null, store)
+
+ expect(store.$state).toEqual({ nestedArr: { arr: [] } })
+
+ defineStore({
+ ...baseOptions,
+ state: () => ({ nestedArr: { arr: [1] } }),
})(null, store)
+ expect(store.$state).toEqual({ nestedArr: { arr: [] } })
})
it('keeps state reactive', () => {
+import { isRef, isReactive } from 'vue'
import { Pinia } from './rootStore'
-import { Store, StoreDefinition, _Method } from './types'
+import { isPlainObject, Store, StoreDefinition, _Method } from './types'
export const isUseStore = (fn: any): fn is StoreDefinition => {
return typeof fn === 'function' && typeof fn.$id === 'string'
}
+export function patchObject(
+ newState: Record<string, any>,
+ oldState: Record<string, any>
+): Record<string, any> {
+ // no need to go through symbols because they cannot be serialized anyway
+ for (const key in oldState) {
+ const subPatch = oldState[key]
+
+ // skip the whole sub tree
+ if (!(key in newState)) {
+ continue
+ }
+
+ const targetValue = newState[key]
+ if (
+ isPlainObject(targetValue) &&
+ isPlainObject(subPatch) &&
+ !isRef(subPatch) &&
+ !isReactive(subPatch)
+ ) {
+ newState[key] = patchObject(targetValue, subPatch)
+ } else {
+ // objects are either a bit more complex (e.g. refs) or primitives, so we
+ // just set the whole thing
+ newState[key] = subPatch
+ }
+ }
+
+ return newState
+}
+
export function acceptHMRUpdate(
initialUseStore: StoreDefinition<string, any, any, any>,
hot: any
activePinia,
} from './rootStore'
import { IS_CLIENT } from './env'
+import { patchObject } from './hmr'
function innerPatch<T extends StateTree>(
target: T,
newStore._hmrPayload.state.forEach((stateKey) => {
if (stateKey in store.$state) {
// @ts-expect-error
- // transfer the ref
- newStore.$state[stateKey] =
- // ---
+ const newStateTarget = newStore.$state[stateKey]
+ // @ts-expect-error
+ const oldStateSource = store.$state[stateKey]
+ if (
+ typeof newStateTarget === 'object' &&
+ isPlainObject(newStateTarget) &&
+ isPlainObject(oldStateSource)
+ ) {
+ patchObject(newStateTarget, oldStateSource)
+ } else {
// @ts-expect-error
- store.$state[stateKey]
+ // transfer the ref
+ newStore.$state[stateKey] = oldStateSource
+ }
}
// patch direct access properties to allow store.stateProperty to work as
// store.$state.stateProperty