From 26aae657280584caf58f3fb474cc09a6ceecfa95 Mon Sep 17 00:00:00 2001 From: webfansplz <308241863@qq.com> Date: Sun, 24 Jul 2022 18:14:08 +0800 Subject: [PATCH] feat(useStorage): `mergeDefaults` option (#1957) Co-authored-by: Anthony Fu --- packages/core/useStorage/index.md | 39 ++++++++++++++++++++++++ packages/core/useStorage/index.test.ts | 27 +++++++++++++++++ packages/core/useStorage/index.ts | 42 ++++++++++++++++++-------- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/packages/core/useStorage/index.md b/packages/core/useStorage/index.md index 24103a8cd6a..ff6fa241d94 100644 --- a/packages/core/useStorage/index.md +++ b/packages/core/useStorage/index.md @@ -32,6 +32,45 @@ const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref< state.value = null ``` +## Merge Defaults + +By default, `useStorage` will use the value from storage if it presents and ignores the default value. Be aware that when you adding more properties to the default value, the key might be undefined if client's storage does not have that key. + +```ts +localStorage.setItem('my-store', '{"hello": "hello"}') + +const state = useStorage('my-store', { hello: 'hi', greeting: 'hello' }, localStorage) + +console.log(state.greeting) // undefined, since the value is not presented in storage +``` + +To solve that, you can enable `mergeDefaults` option. + +```ts +localStorage.setItem('my-store', '{"hello": "nihao"}') + +const state = useStorage( + 'my-store', + { hello: 'hi', greeting: 'hello' }, + localStorage, + { mergeDefaults: true } // <-- +) + +console.log(state.hello) // 'nihao', from storage +console.log(state.greeting) // 'hello', from merged default value +``` + +When setting it to true, it will perform a **shallow merge** for objects. You can pass a function to perform custom merge (e.g. deep merge), for example: + +```ts +const state = useStorage( + 'my-store', + { hello: 'hi', greeting: 'hello' }, + localStorage, + { mergeDefaults: (storageValue, defaults) => deepMerge(defaults, storageValue) } // <-- +) +``` + ## Custom Serialization By default, `useStorage` will smartly use the corresponding serializer based on the data type of provided default value. For example, `JSON.stringify` / `JSON.parse` will be used for objects, `Number.toString` / `parseFloat` for numbers, etc. diff --git a/packages/core/useStorage/index.test.ts b/packages/core/useStorage/index.test.ts index e8009a9717a..51b2e563c3d 100644 --- a/packages/core/useStorage/index.test.ts +++ b/packages/core/useStorage/index.test.ts @@ -331,4 +331,31 @@ describe('useStorage', () => { expect(storage.removeItem).toBeCalledWith(KEY) }) + + it('mergeDefaults option', async () => { + // basic + storage.setItem(KEY, '0') + const basicRef = useStorage(KEY, 1, storage, { mergeDefaults: true }) + expect(basicRef.value).toBe(0) + + // object + storage.setItem(KEY, JSON.stringify({ a: 1 })) + const objectRef = useStorage(KEY, { a: 2, b: 3 }, storage, { mergeDefaults: true }) + expect(objectRef.value).toEqual({ a: 1, b: 3 }) + + // array + storage.setItem(KEY, JSON.stringify([1])) + const arrayRef = useStorage(KEY, [2], storage, { mergeDefaults: true }) + expect(arrayRef.value).toEqual([1]) + + // custom function + storage.setItem(KEY, JSON.stringify([{ a: 1 }])) + const customRef = useStorage(KEY, [{ a: 3 }], storage, { mergeDefaults: (value, initial) => [...initial, ...value] }) + expect(customRef.value).toEqual([{ a: 3 }, { a: 1 }]) + + // custom function 2 + storage.setItem(KEY, '1') + const customRef2 = useStorage(KEY, 2, storage, { mergeDefaults: (value, initial) => value + initial }) + expect(customRef2.value).toEqual(3) + }) }) diff --git a/packages/core/useStorage/index.ts b/packages/core/useStorage/index.ts index f4eb796b9ce..f2ca68b9e52 100644 --- a/packages/core/useStorage/index.ts +++ b/packages/core/useStorage/index.ts @@ -1,5 +1,5 @@ import type { Awaitable, ConfigurableEventFilter, ConfigurableFlush, MaybeComputedRef, RemovableRef } from '@vueuse/shared' -import { pausableWatch, resolveUnref } from '@vueuse/shared' +import { isFunction, pausableWatch, resolveUnref } from '@vueuse/shared' import { ref, shallowRef } from 'vue-demi' import type { StorageLike } from '../ssr-handlers' import { getSSRHandler } from '../ssr-handlers' @@ -75,6 +75,16 @@ export interface UseStorageOptions extends ConfigurableEventFilter, Configura */ writeDefaults?: boolean + /** + * Merge the default value with the value read from the storage. + * + * When setting it to true, it will perform a **shallow merge** for objects. + * You can pass a function to perform custom merge (e.g. deep merge), for example: + * + * @default false + */ + mergeDefaults?: boolean | ((storageValue: T, defaults: T) => T) + /** * Custom data serialization */ @@ -95,24 +105,20 @@ export interface UseStorageOptions extends ConfigurableEventFilter, Configura shallow?: boolean } -export function useStorage(key: string, initialValue: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef -export function useStorage(key: string, initialValue: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef -export function useStorage(key: string, initialValue: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef -export function useStorage(key: string, initialValue: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef -export function useStorage(key: string, initialValue: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef +export function useStorage(key: string, defaults: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef +export function useStorage(key: string, defaults: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef +export function useStorage(key: string, defaults: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef +export function useStorage(key: string, defaults: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef +export function useStorage(key: string, defaults: MaybeComputedRef, storage?: StorageLike, options?: UseStorageOptions): RemovableRef /** * Reactive LocalStorage/SessionStorage. * * @see https://vueuse.org/useStorage - * @param key - * @param initialValue - * @param storage - * @param options */ export function useStorage( key: string, - initialValue: MaybeComputedRef, + defaults: MaybeComputedRef, storage: StorageLike | undefined, options: UseStorageOptions = {}, ): RemovableRef { @@ -121,6 +127,7 @@ export function useStorage deep = true, listenToStorageChanges = true, writeDefaults = true, + mergeDefaults = false, shallow, window = defaultWindow, eventFilter, @@ -128,7 +135,8 @@ export function useStorage console.error(e) }, } = options - const data = (shallow ? shallowRef : ref)(initialValue) as RemovableRef + + const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef if (!storage) { try { @@ -142,7 +150,7 @@ export function useStorage if (!storage) return data - const rawInit: T = resolveUnref(initialValue) + const rawInit: T = resolveUnref(defaults) const type = guessSerializerType(rawInit) const serializer = options.serializer ?? StorageSerializers[type] @@ -186,6 +194,14 @@ export function useStorage storage!.setItem(key, serializer.write(rawInit)) return rawInit } + else if (!event && mergeDefaults) { + const value = serializer.read(rawValue) + if (isFunction(mergeDefaults)) + return mergeDefaults(value, rawInit) + else if (type === 'object' && !Array.isArray(value)) + return { ...rawInit as any, ...value } + return value + } else if (typeof rawValue !== 'string') { return rawValue }