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(useIDBKeyval): new integration - Idb-keyval wrapper #2335
Changes from 6 commits
f1c45ea
729aad8
ebd5474
ef604ad
df06d83
7f61444
cf8e52f
fe96080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Supports polyfilling window, web workers, as well as node's global | ||
import 'fake-indexeddb/auto' | ||
|
||
export {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<script setup lang="ts"> | ||
import { stringify } from '@vueuse/docs-utils' | ||
import { useIDBKeyval } from '@vueuse/integrations' | ||
|
||
const state = useIDBKeyval('vue-use-idb-keyval', { | ||
name: 'Banana', | ||
color: 'Yellow', | ||
size: 'Medium', | ||
count: 0, | ||
}) | ||
|
||
const text = stringify(state) | ||
</script> | ||
|
||
<template> | ||
<input v-model="state.name" type="text"> | ||
<input v-model="state.color" type="text"> | ||
<input v-model="state.size" type="text"> | ||
<input v-model.number="state.count" type="range" min="0" step="0.01" max="1000"> | ||
|
||
<pre lang="json">{{ text }}</pre> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
--- | ||
category: '@Integrations' | ||
--- | ||
|
||
# useIDBKeyval | ||
|
||
Wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval). | ||
|
||
|
||
## Install idb-keyval as a peer dependency | ||
|
||
```bash | ||
npm install idb-keyval | ||
``` | ||
|
||
## Usage | ||
|
||
```ts | ||
import { useIDBKeyval } from '@vueuse/integrations' | ||
|
||
// bind object | ||
const storedObject = useIDBKeyval('my-idb-keyval-store', { hello: 'hi', greeting: 'Hello' }) | ||
|
||
// update object | ||
storedObject.value.hello = 'hola' | ||
|
||
// bind boolean | ||
const flag = useIDBKeyval('my-flag', true) // returns Ref<boolean> | ||
|
||
// bind number | ||
const count = useIDBKeyval('my-count', 0) // returns Ref<number> | ||
|
||
// delete data from idb storage | ||
storedObject.value = null | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { nextTick } from 'vue-demi' | ||
import { del, get, set, update } from 'idb-keyval' | ||
import { useIDBKeyval } from '.' | ||
|
||
const KEY = 'vue-use-idb-keyval' | ||
|
||
const defaultState = { | ||
name: 'Banana', | ||
color: 'Yellow', | ||
size: 'Medium', | ||
count: 0, | ||
} | ||
|
||
beforeEach(() => { | ||
vi.unmock('idb-keyval') | ||
vi.mock('idb-keyval') | ||
}) | ||
|
||
describe('useIDBKeyval', () => { | ||
it('set/get', async () => { | ||
const state = useIDBKeyval(KEY, defaultState) | ||
await nextTick() | ||
expect(get).toHaveBeenCalled() | ||
expect(set).toHaveBeenCalled() | ||
expect(state.value).toEqual(defaultState) | ||
}) | ||
|
||
it('update', async () => { | ||
const state = useIDBKeyval(KEY, defaultState) | ||
state.value.name = 'Apple' | ||
state.value.color = 'Red' | ||
state.value.size = 'Giant' | ||
state.value.count += 1 | ||
|
||
await nextTick() | ||
expect(update).toHaveBeenCalled() | ||
|
||
expect(state.value.name).toBe('Apple') | ||
expect(state.value.color).toBe('Red') | ||
expect(state.value.size).toBe('Giant') | ||
expect(state.value.count).toBe(1) | ||
}) | ||
|
||
it('del', async () => { | ||
const state = useIDBKeyval(KEY, defaultState) | ||
state.value = null | ||
await nextTick() | ||
sxzz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
expect(del).toHaveBeenCalled() | ||
}) | ||
|
||
it('string', async () => { | ||
const state = useIDBKeyval(KEY, 'foo') | ||
expect(get).toHaveBeenCalled() | ||
expect(set).toHaveBeenCalled() | ||
expect(state.value).toEqual('foo') | ||
state.value = 'bar' | ||
expect(state.value).toEqual('bar') | ||
expect(update).toHaveBeenCalled() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import type { ConfigurableFlush, MaybeComputedRef, RemovableRef } from '@vueuse/shared' | ||
import { resolveUnref } from '@vueuse/shared' | ||
import type { Ref } from 'vue-demi' | ||
import { ref, shallowRef, watch } from 'vue-demi' | ||
import { del, get, set, update } from 'idb-keyval' | ||
|
||
export interface UseIDBOptions extends ConfigurableFlush { | ||
/** | ||
* Watch for deep changes | ||
* | ||
* @default true | ||
*/ | ||
deep?: boolean | ||
|
||
/** | ||
* On error callback | ||
* | ||
* Default log error to `console.error` | ||
*/ | ||
onError?: (error: unknown) => void | ||
|
||
/** | ||
* Use shallow ref as reference | ||
* | ||
* @default false | ||
*/ | ||
shallow?: boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove this option and the associated code by having the user pass in a computed, ref, or shallowRef? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was following the same pattern as |
||
} | ||
|
||
/** | ||
* | ||
* @param key | ||
* @param initialValue | ||
* @param options | ||
*/ | ||
export function useIDBKeyval<T>( | ||
key: IDBValidKey, | ||
initialValue: MaybeComputedRef<T>, | ||
options: UseIDBOptions = {}, | ||
): RemovableRef<T> { | ||
const { | ||
flush = 'pre', | ||
deep = true, | ||
shallow, | ||
onError = (e) => { | ||
console.error(e) | ||
}, | ||
} = options | ||
|
||
const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T> | ||
|
||
const rawInit: T = resolveUnref(initialValue) | ||
|
||
async function read() { | ||
try { | ||
const rawValue = await get<T>(key) | ||
if (rawValue === undefined) { | ||
if (rawInit) | ||
set(key, rawInit) | ||
} | ||
else { | ||
data.value = rawValue | ||
} | ||
} | ||
catch (e) { | ||
onError(e) | ||
} | ||
} | ||
|
||
read() | ||
|
||
async function write() { | ||
try { | ||
if (data.value == null) | ||
await del(key) | ||
else | ||
await update(key, () => ({ ...data.value })) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question, did this have the same issue that you were messaging me about? The issue with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah looks like that one is having an issue too. This is what jimmy and I added when we worked through the bug the other day, it had something to do with proxy objects not able to be stored in index db if I remember correctly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed this logic to only make copy with spread syntax if data.value is an object or array |
||
} | ||
catch (e) { | ||
onError(e) | ||
} | ||
} | ||
|
||
watch(data, () => write(), { flush, deep }) | ||
|
||
return data as RemovableRef<T> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also requires to be added to peer deps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added to peer deps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sxzz Regarding the first comment, thank you for your patience as I looked into this.
I looked through
useLocalStorage
anduseSessionStorage
to understand how they reuseuseStorage
. I believe you are suggesting that we could use indexDB storage in place ofwindow?.localStorage
, the problem I see is thatuseStorage
converts all values to strings, and a big reason our team wanted to use indexDB is so we can store values other than just strings.I also took a closer look at
StorageLike
, its get and set functions get and return strings, so I am not sure that using indexDB in this way would provide any gains to usinglocalStorage
as is.Please let me know if I am misunderstanding something.