Skip to content

Commit

Permalink
feat(useStorage): mergeDefaults option (#1957)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
webfansplz and antfu committed Jul 24, 2022
1 parent 085be3c commit 26aae65
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 13 deletions.
39 changes: 39 additions & 0 deletions packages/core/useStorage/index.md
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions packages/core/useStorage/index.test.ts
Expand Up @@ -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)
})
})
42 changes: 29 additions & 13 deletions 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'
Expand Down Expand Up @@ -75,6 +75,16 @@ export interface UseStorageOptions<T> 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
*/
Expand All @@ -95,24 +105,20 @@ export interface UseStorageOptions<T> extends ConfigurableEventFilter, Configura
shallow?: boolean
}

export function useStorage(key: string, initialValue: MaybeComputedRef<string>, storage?: StorageLike, options?: UseStorageOptions<string>): RemovableRef<string>
export function useStorage(key: string, initialValue: MaybeComputedRef<boolean>, storage?: StorageLike, options?: UseStorageOptions<boolean>): RemovableRef<boolean>
export function useStorage(key: string, initialValue: MaybeComputedRef<number>, storage?: StorageLike, options?: UseStorageOptions<number>): RemovableRef<number>
export function useStorage<T>(key: string, initialValue: MaybeComputedRef<T>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>
export function useStorage<T = unknown>(key: string, initialValue: MaybeComputedRef<null>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>
export function useStorage(key: string, defaults: MaybeComputedRef<string>, storage?: StorageLike, options?: UseStorageOptions<string>): RemovableRef<string>
export function useStorage(key: string, defaults: MaybeComputedRef<boolean>, storage?: StorageLike, options?: UseStorageOptions<boolean>): RemovableRef<boolean>
export function useStorage(key: string, defaults: MaybeComputedRef<number>, storage?: StorageLike, options?: UseStorageOptions<number>): RemovableRef<number>
export function useStorage<T>(key: string, defaults: MaybeComputedRef<T>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>
export function useStorage<T = unknown>(key: string, defaults: MaybeComputedRef<null>, storage?: StorageLike, options?: UseStorageOptions<T>): RemovableRef<T>

/**
* Reactive LocalStorage/SessionStorage.
*
* @see https://vueuse.org/useStorage
* @param key
* @param initialValue
* @param storage
* @param options
*/
export function useStorage<T extends(string | number | boolean | object | null)>(
key: string,
initialValue: MaybeComputedRef<T>,
defaults: MaybeComputedRef<T>,
storage: StorageLike | undefined,
options: UseStorageOptions<T> = {},
): RemovableRef<T> {
Expand All @@ -121,14 +127,16 @@ export function useStorage<T extends(string | number | boolean | object | null)>
deep = true,
listenToStorageChanges = true,
writeDefaults = true,
mergeDefaults = false,
shallow,
window = defaultWindow,
eventFilter,
onError = (e) => {
console.error(e)
},
} = options
const data = (shallow ? shallowRef : ref)(initialValue) as RemovableRef<T>

const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>

if (!storage) {
try {
Expand All @@ -142,7 +150,7 @@ export function useStorage<T extends(string | number | boolean | object | null)>
if (!storage)
return data

const rawInit: T = resolveUnref(initialValue)
const rawInit: T = resolveUnref(defaults)
const type = guessSerializerType<T>(rawInit)
const serializer = options.serializer ?? StorageSerializers[type]

Expand Down Expand Up @@ -186,6 +194,14 @@ export function useStorage<T extends(string | number | boolean | object | null)>
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
}
Expand Down

0 comments on commit 26aae65

Please sign in to comment.