Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useStorage): mergeDefaults option #1957

Merged
merged 8 commits into from Jul 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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