Skip to content

Commit 3a2df2e

Browse files
asika32764ferfergaOrbisKautofix-ci[bot]antfu
authored
feat(useStorageAsync): add onReady option and Promise return (#4158)
Co-authored-by: Fernando Fernández <ferferga@hotmail.com> Co-authored-by: Robin <robin.kehl@singular-it.de> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent c92a8ff commit 3a2df2e

File tree

3 files changed

+183
-9
lines changed

3 files changed

+183
-9
lines changed

packages/core/useStorageAsync/index.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,81 @@ Reactive Storage in with async support.
88

99
## Usage
1010

11-
Please refer to `useStorage`.
11+
The basic usage please refer to `useStorage`.
12+
13+
## Wait First Loaded
14+
15+
When user entering your app, `useStorageAsync()` will start loading value from an async storage,
16+
sometimes you may get the default initial value, not the real value stored in storage at the very
17+
beginning.
18+
19+
```ts
20+
import { useStorageAsync } from '@vueuse/core'
21+
22+
const accessToken = useStorageAsync('access.token', '', SomeAsyncStorage)
23+
24+
// accessToken.value may be empty before the async storage is ready
25+
console.log(accessToken.value) // ""
26+
27+
setTimeout(() => {
28+
// After some time, the async storage is ready
29+
console.log(accessToken.value) // "the real value stored in storage"
30+
}, 500)
31+
```
32+
33+
In this case, you can wait the storage prepared, the returned value is also a `Promise`,
34+
so you can wait it resolved in your template or script.
35+
36+
```ts
37+
// Use top-level await if your environment supports it
38+
const accessToken = await useStorageAsync('access.token', '', SomeAsyncStorage)
39+
40+
console.log(accessToken.value) // "the real value stored in storage"
41+
```
42+
43+
If you must wait multiple storages, put them into a `Promise.allSettled()`
44+
45+
```ts
46+
router.onReady(async () => {
47+
await Promise.allSettled([
48+
accessToken,
49+
refreshToken,
50+
userData,
51+
])
52+
53+
app.mount('app')
54+
})
55+
```
56+
57+
There is a callback named `onReady` in options:
58+
59+
```ts
60+
import { useStorageAsync } from '@vueuse/core'
61+
62+
// Use ES2024 Promise.withResolvers, you may use any Deferred object or EventBus to do same thing.
63+
const { promise, resolve } = Promise.withResolvers()
64+
65+
const accessToken = useStorageAsync('access.token', '', SomeAsyncStorage, {
66+
onReady(value) {
67+
resolve(value)
68+
}
69+
})
70+
71+
// At main.ts
72+
router.onReady(async () => {
73+
// Let's wait accessToken loaded
74+
await promise
75+
76+
// Now accessToken has loaded, we can safely mount our app
77+
78+
app.mount('app')
79+
})
80+
```
81+
82+
Simply use `resolve` as callback:
83+
84+
```ts
85+
const accessToken = useStorageAsync('access.token', '', SomeAsyncStorage, {
86+
onReady: resolve
87+
})
88+
```
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { Awaitable, StorageLikeAsync } from '@vueuse/core'
2+
import { createEventHook, useStorageAsync } from '@vueuse/core'
3+
import { beforeEach, describe, expect, it } from 'vitest'
4+
5+
const KEY = 'custom-key'
6+
const KEY2 = 'custom-key2'
7+
const asyncDelay = 10
8+
const localStorage = globalThis.localStorage
9+
10+
class AsyncStubStorage implements StorageLikeAsync {
11+
getItem(key: string): Awaitable<string | null> {
12+
return new Promise((resolve) => {
13+
setTimeout(() => {
14+
resolve(localStorage.getItem(key))
15+
}, asyncDelay)
16+
})
17+
}
18+
19+
removeItem(key: string): Awaitable<void> {
20+
return new Promise((resolve) => {
21+
setTimeout(() => {
22+
localStorage.removeItem(key)
23+
resolve()
24+
}, asyncDelay)
25+
})
26+
}
27+
28+
setItem(key: string, value: string): Awaitable<void> {
29+
return new Promise((resolve) => {
30+
setTimeout(() => {
31+
localStorage.setItem(key, value)
32+
resolve()
33+
}, asyncDelay)
34+
})
35+
}
36+
}
37+
38+
describe('useStorageAsync', () => {
39+
beforeEach(() => {
40+
localStorage.clear()
41+
})
42+
43+
it('onReady', async () => {
44+
localStorage.setItem(KEY, 'CurrentValue')
45+
46+
const loaded = createEventHook()
47+
const promise = new Promise<string>((resolve) => {
48+
loaded.on(resolve)
49+
})
50+
51+
const storage = useStorageAsync(
52+
KEY,
53+
'',
54+
new AsyncStubStorage(),
55+
{
56+
onReady(value) {
57+
loaded.trigger(value)
58+
},
59+
},
60+
)
61+
62+
expect(storage.value).toBe('')
63+
await expect(promise).resolves.toBe('CurrentValue')
64+
})
65+
66+
it('onReadyByPromise', async () => {
67+
localStorage.setItem(KEY2, 'AnotherValue')
68+
69+
const storage = useStorageAsync(
70+
KEY2,
71+
'',
72+
new AsyncStubStorage(),
73+
)
74+
75+
expect(storage.value).toBe('')
76+
77+
storage.then((result) => {
78+
expect(result.value).toBe('AnotherValue')
79+
})
80+
})
81+
})

packages/core/useStorageAsync/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ export interface UseStorageAsyncOptions<T> extends Omit<UseStorageOptions<T>, 's
1515
* Custom data serialization
1616
*/
1717
serializer?: SerializerAsync<T>
18+
19+
/**
20+
* On first value loaded hook.
21+
*/
22+
onReady?: (value: T) => void
1823
}
1924

20-
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<string>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<string>): RemovableRef<string>
21-
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<boolean>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<boolean>): RemovableRef<boolean>
22-
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<number>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<number>): RemovableRef<number>
23-
export function useStorageAsync<T>(key: string, initialValue: MaybeRefOrGetter<T>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T>
24-
export function useStorageAsync<T = unknown>(key: string, initialValue: MaybeRefOrGetter<null>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T>
25+
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<string>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<string>): RemovableRef<string> & Promise<RemovableRef<string>>
26+
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<boolean>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<boolean>): RemovableRef<boolean> & Promise<RemovableRef<boolean>>
27+
export function useStorageAsync(key: string, initialValue: MaybeRefOrGetter<number>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<number>): RemovableRef<number> & Promise<RemovableRef<number>>
28+
export function useStorageAsync<T>(key: string, initialValue: MaybeRefOrGetter<T>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T> & Promise<RemovableRef<T>>
29+
export function useStorageAsync<T = unknown>(key: string, initialValue: MaybeRefOrGetter<null>, storage?: StorageLikeAsync, options?: UseStorageAsyncOptions<T>): RemovableRef<T> & Promise<RemovableRef<T>>
2530

2631
/**
2732
* Reactive Storage in with async support.
@@ -37,7 +42,7 @@ export function useStorageAsync<T extends(string | number | boolean | object | n
3742
initialValue: MaybeRefOrGetter<T>,
3843
storage: StorageLikeAsync | undefined,
3944
options: UseStorageAsyncOptions<T> = {},
40-
): RemovableRef<T> {
45+
): RemovableRef<T> & Promise<RemovableRef<T>> {
4146
const {
4247
flush = 'pre',
4348
deep = true,
@@ -50,6 +55,7 @@ export function useStorageAsync<T extends(string | number | boolean | object | n
5055
onError = (e) => {
5156
console.error(e)
5257
},
58+
onReady,
5359
} = options
5460

5561
const rawInit: T = toValue(initialValue)
@@ -95,7 +101,12 @@ export function useStorageAsync<T extends(string | number | boolean | object | n
95101
}
96102
}
97103

98-
read()
104+
const promise = new Promise((resolve) => {
105+
read().then(() => {
106+
onReady?.(data.value)
107+
resolve(data)
108+
})
109+
})
99110

100111
if (window && listenToStorageChanges)
101112
useEventListener(window, 'storage', e => Promise.resolve().then(() => read(e)), { passive: true })
@@ -122,5 +133,10 @@ export function useStorageAsync<T extends(string | number | boolean | object | n
122133
)
123134
}
124135

125-
return data
136+
Object.assign(data, {
137+
then: promise.then.bind(promise),
138+
catch: promise.catch.bind(promise),
139+
})
140+
141+
return data as RemovableRef<T> & Promise<RemovableRef<T>>
126142
}

0 commit comments

Comments
 (0)