Repository: pmndrs/zustand (ts)

Issue:
bug(middleware/persist): using new storage option with createJSONStorage causes subscription to fire early (regression)
## Summary

using old `getStorage` option would not fire subscriptions when retrieving the state from storage for the first time, but `storage` does

## Link to reproduction

old: https://codesandbox.io/s/autumn-fog-400vf4
new: https://codesandbox.io/s/happy-carson-0teelt

## Check List

- [x] I've already opened a [discussion](https://github.com/pmndrs/zustand/discussions) before opening this issue.


Pull request:
fix(middleware/persist): hydrate in sync (new impl with storage option)
## Related Issues

Fixes #1517.

## Summary

In #1463, I hoped to avoid `toThenable()` whose implementation is complicated, and used `Promise.resolve()` instead. I wasn't aware this is a regression because it didn't migrate the tests.

This PR fixes the tests, and use `toThenable`.

## Check List

- [ ] `yarn run prettier` for formatting code and docs


Patch:
diff --git a/docs/integrations/persisting-store-data.md b/docs/integrations/persisting-store-data.md
index 2947009..a64fc77 100644
--- a/docs/integrations/persisting-store-data.md
+++ b/docs/integrations/persisting-store-data.md
@@ -571,3 +571,39 @@ export const useBearStore = create<MyState>()(
   )
 )
 ```
+
+### How do I use it with Map and Set?
+
+With the previous persist API, you would use `serialize`/`deserialize`
+to deal with `Map` and `Set` and convert them into
+an Array so they could be parsed into proper JSON.
+
+The new persist API has deprecated `serialize`/`deserialize`.
+
+Now, you will need to use the `storage` prop.
+Let's say your state uses `Map` to handle a list of `transactions`,
+then you can convert the Map into an Array in the storage prop:
+
+```ts
+  storage: {
+    getItem: (name) => {
+      const str = localStorage.getItem(name)
+      return {
+        state: {
+          ...JSON.parse(str).state,
+          transactions: new Map(JSON.parse(str).state.transactions),
+        },
+      }
+    },
+    setItem: (name, newValue) => {
+      const str = JSON.stringify({
+        state: {
+          ...newValue.state,
+          transactions: Array.from(newValue.state.transactions.entries()),
+        },
+      })
+      localStorage.setItem(name, str)
+    },
+    removeItem: (name) => localStorage.removeItem(name),
+  },
+```
diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts
index 43b1865..c84424e 100644
--- a/src/middleware/persist.ts
+++ b/src/middleware/persist.ts
@@ -418,7 +418,7 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
       options.onRehydrateStorage?.(get()) || undefined
 
     // bind is used to avoid `TypeError: Illegal invocation` error
-    return Promise.resolve(storage.getItem.bind(storage)(options.name))
+    return toThenable(storage.getItem.bind(storage))(options.name)
       .then((deserializedStorageValue) => {
         if (deserializedStorageValue) {
           if (
@@ -473,7 +473,7 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
       storage?.removeItem(options.name)
     },
     getOptions: () => options,
-    rehydrate: () => hydrate(),
+    rehydrate: () => hydrate() as Promise<void>,
     hasHydrated: () => hasHydrated,
     onHydrate: (cb) => {
       hydrationListeners.add(cb)


Test patch:
diff --git a/tests/persistAsync.test.tsx b/tests/persistAsync.test.tsx
index ae6f1db..a6ead44 100644
--- a/tests/persistAsync.test.tsx
+++ b/tests/persistAsync.test.tsx
@@ -1,7 +1,7 @@
 import { StrictMode, useEffect } from 'react'
 import { act, render, waitFor } from '@testing-library/react'
 import { create } from 'zustand'
-import { persist } from 'zustand/middleware'
+import { createJSONStorage, persist } from 'zustand/middleware'
 
 const createPersistantStore = (initialValue: string | null) => {
   let state = initialValue
@@ -58,7 +58,7 @@ describe('persist middleware with async configuration', () => {
         }),
         {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
           onRehydrateStorage: () => onRehydrateStorageSpy,
         }
       )
@@ -101,7 +101,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
       })
     )
@@ -134,7 +134,7 @@ describe('persist middleware with async configuration', () => {
       const useBoundStore = create(
         persist(() => ({ count: 0 }), {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
           onRehydrateStorage: () => onRehydrateStorageSpy,
         })
       )
@@ -208,7 +208,7 @@ describe('persist middleware with async configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
         migrate: migrateSpy,
       })
@@ -263,7 +263,7 @@ describe('persist middleware with async configuration', () => {
         }),
         {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
         }
       )
     )
@@ -315,7 +315,7 @@ describe('persist middleware with async configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
       })
     )
@@ -356,7 +356,7 @@ describe('persist middleware with async configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         migrate: () => {
           throw new Error('migrate error')
         },
@@ -402,7 +402,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0, unstorableMethod }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
       })
     )
@@ -446,7 +446,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0, actions: { unstorableMethod } }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         merge: (_persistedState, currentState) => {
           const persistedState = _persistedState as any
           delete persistedState.actions
@@ -494,8 +494,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
-        deserialize: (str) => JSON.parse(str),
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -528,7 +527,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -549,7 +548,7 @@ describe('persist middleware with async configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
     expect(useBoundStore.persist.hasHydrated()).toBe(false)
diff --git a/tests/persistSync.test.tsx b/tests/persistSync.test.tsx
index 3e2a7a1..b2e1df4 100644
--- a/tests/persistSync.test.tsx
+++ b/tests/persistSync.test.tsx
@@ -1,5 +1,5 @@
 import { create } from 'zustand'
-import { persist } from 'zustand/middleware'
+import { createJSONStorage, persist } from 'zustand/middleware'
 
 const createPersistentStore = (initialValue: string | null) => {
   let state = initialValue
@@ -54,7 +54,7 @@ describe('persist middleware with sync configuration', () => {
         }),
         {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
           onRehydrateStorage: () => onRehydrateStorageSpy,
         }
       )
@@ -83,7 +83,7 @@ describe('persist middleware with sync configuration', () => {
     create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => spy,
       })
     )
@@ -99,7 +99,7 @@ describe('persist middleware with sync configuration', () => {
       const useBoundStore = create(
         persist(() => ({ count: 0 }), {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
           onRehydrateStorage: () => onRehydrateStorageSpy,
         })
       )
@@ -148,7 +148,7 @@ describe('persist middleware with sync configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
         migrate: migrateSpy,
       })
@@ -183,7 +183,7 @@ describe('persist middleware with sync configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
       })
     )
@@ -210,7 +210,7 @@ describe('persist middleware with sync configuration', () => {
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
         version: 13,
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         migrate: () => {
           throw new Error('migrate error')
         },
@@ -243,7 +243,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0, unstorableMethod }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         onRehydrateStorage: () => onRehydrateStorageSpy,
       })
     )
@@ -273,7 +273,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0, actions: { unstorableMethod } }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         merge: (_persistedState, currentState) => {
           const persistedState = _persistedState as any
           delete persistedState.actions
@@ -309,8 +309,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
-        deserialize: (str) => JSON.parse(str),
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -349,7 +348,7 @@ describe('persist middleware with sync configuration', () => {
         }),
         {
           name: 'test-storage',
-          getStorage: () => storage,
+          storage: createJSONStorage(() => storage),
           partialize: (state) => {
             return {
               object: {
@@ -394,7 +393,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
     expect(useBoundStore.persist.getOptions().name).toBeDefined()
@@ -413,7 +412,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
         partialize: (s) => s as Partial<typeof s>,
       })
     )
@@ -450,7 +449,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -470,7 +469,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -491,7 +490,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )
 
@@ -519,7 +518,7 @@ describe('persist middleware with sync configuration', () => {
     const useBoundStore = create(
       persist(() => ({ count: 0 }), {
         name: 'test-storage',
-        getStorage: () => storage,
+        storage: createJSONStorage(() => storage),
       })
     )