Integrations
Vue plugin
Install the plugin to configure a global key prefix, default target, and error handler.
import { createApp } from 'vue'
import { VueStoragePlugin } from 'vue-storage-kit'
import App from './App.vue'
const app = createApp(App)
app.use(VueStoragePlugin, {
prefix: 'myapp:', // all keys are prefixed automatically
defaultTarget: 'local',
onError: (err) => {
if (err.type === 'quota-exceeded') showNotification('Storage full')
},
})
app.mount('#app')VueStoragePluginOptions
These apply to every useStorage() call (and anything built on it, like useStorageList()) made after the plugin is installed — not to useCookie, useIndexedDB/useIDBRef, or createPiniaPersist, which have their own independent options and don't read from the plugin.
| Option | Type | Description |
|---|---|---|
prefix | string | Prepended to every storage key — useStorage('counter', ...) actually reads/writes myapp:counter. Two useStorage() calls for the same logical key installed with different prefixes are treated as distinct instances |
defaultTarget | StorageTarget | Used when a call doesn't pass target itself; an explicit target (including the one baked into useLocalStorage/useSessionStorage) always wins |
defaultSerializer | Serializer<unknown> | Fallback used when a call doesn't pass its own serializer |
defaultEncrypt | EncryptOptions | With encrypt: true, used as-is. With encrypt: { ... }, the call's options are merged on top — e.g. encrypt: { iterations: 200_000 } can override just one field while the password still comes from here |
onError | (err: StorageError) => void | Called in addition to (not instead of) any per-call onError — handy for app-wide logging/telemetry alongside call-site-specific handling |
Devtools
A custom Vue Devtools inspector and timeline, built on the shared engine cache — so it shows every live useStorage() instance regardless of whether it was created from Vue or from the React hook.
- Inspector: key, target, current value,
isReady,expiry,canUndo/canRedo, and error state — refreshed roughly once a second so cross-tab or TTL-driven changes show up without a manual refresh. - Timeline: logs
write,expire,migrate,sync-received, anderrorevents as they happen, so you can see when and why a value changed, not just its current snapshot.
Opt-in: call setupDevtools(app) from the /devtools entry point once, wherever you create your app.
import { createApp } from 'vue'
import { setupDevtools } from 'vue-storage-kit/devtools'
import App from './App.vue'
const app = createApp(App)
setupDevtools(app)
app.mount('#app')It's safe to call unconditionally — setupDevtools (and the @vue/devtools-api it wraps) no-ops when no devtools client is connected, so calling it in production has no effect beyond the (tiny) added code. If you'd rather strip it entirely from production bundles, gate the call and the import behind your own dev-mode check:
if (import.meta.env.DEV) {
const { setupDevtools } = await import('vue-storage-kit/devtools')
setupDevtools(app)
}Nuxt module
Add the module in nuxt.config.ts to auto-import all composables and register the plugin with a prefix from runtimeConfig.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['vue-storage-kit/nuxt'],
storageKit: {
prefix: 'myapp_',
autoImports: true, // default: true
},
})With autoImports: true the following are available globally without an explicit import. useCookie here resolves to the SSR-aware runtime version (H3-backed on the server), not the client-only one exported from the package root:
useStorage()
useLocalStorage()
useSessionStorage()
useIndexedDB()
useIDBRef()
useCookie()SSR caveat: the
StorageEngineinstance cache (keyed bytarget:key, shared across both the Vue and React bindings) is a module-level singleton per server process, not per-request.target: 'local'/'session'already fail closed todefaultValueserver-side (there's nowindowin Node).target: 'memory'/'indexeddb', however, have no such guard and would share state across concurrent requests on the same server if used during SSR — avoid those targets for per-request/per-user data server-side; they're intended for client-side use.
React support
The /react entry point exports a useStorage() hook built on the same framework-agnostic engine as the Vue composable — same options (TTL, migrations, encrypt, compress, sign, sync, debounce/throttle, history, evictOnQuota), same behavior. It's backed by React's useSyncExternalStore, so it's safe under concurrent rendering.
import { useStorage } from 'vue-storage-kit/react'
function Counter() {
const {
value: count,
setValue: setCount,
isReady,
} = useStorage('count', {
defaultValue: 0,
target: 'local',
})
if (!isReady) return <p>Loading…</p>
return <button onClick={() => setCount((c) => c + 1)}>Clicked {count} times</button>
}Differences from the Vue composable
| Vue | React | |
|---|---|---|
| Returns | { value: Ref<T>, ... } — assign value.value = x to write | { value: T, setValue, ... } — call setValue(x) or setValue(prev => next) to write |
| Shared instance | Two Vue components with the same key+target share one Ref | Two React components with the same key+target share one underlying engine (via useSyncExternalStore), but each gets its own snapshot |
Reacting to a changed key | Not supported — same as the Vue side | Not supported. Mount a new component instance for a different key (e.g. via a key prop), the same pattern React already recommends for "reset this state" |
Two Vue components, two React components, or a mix of both, calling useStorage() with the same key+target all share one underlying engine — one set of timers, one adapter call per write, one TTL/migration/sync pipeline — regardless of which framework(s) created them.
Not yet available for React (Vue-only for now — see the project's todo.md for the tracked backlog): useCookie, useIndexedDB/useIDBRef, useStorageList, useStorageKeys, useBroadcastChannel, and a Pinia-persist equivalent.
react (^18.0.0, for useSyncExternalStore) is an optional peer dependency — only required if you import vue-storage-kit/react.
Testing utilities
The /testing entry point packages the patterns this package's own test suite uses everywhere — no test-runner-specific import (works with Vitest, Jest, or anything else, since it just reassigns a plain object property, not vi.spyOn).
import {
mockStorage,
resetStorageState,
seedExpiredEnvelope,
flushAsync,
} from 'vue-storage-kit/testing'
import { useStorage } from 'vue-storage-kit'
beforeEach(() => {
resetStorageState() // clears the shared instance/engine cache between tests
})
it('reads an existing value', async () => {
const { adapter, restore } = mockStorage() // redirects every target to one MemoryStorageAdapter
await adapter.setItem('k', JSON.stringify({ v: 1, d: '"stored"', exp: null, ts: Date.now() }))
const { value } = useStorage('k', { defaultValue: 'default', target: 'memory' })
await flushAsync()
expect(value.value).toBe('stored')
restore()
})
it('treats an expired key as expired', async () => {
const { adapter } = mockStorage()
await seedExpiredEnvelope(adapter, 'k', 'stale') // shorthand for the envelope above, with exp in the past
const { value } = useStorage('k', { defaultValue: 'default', target: 'memory' })
await flushAsync()
expect(value.value).toBe('default')
})| Export | Description |
|---|---|
mockStorage(adapter?) | Redirects StorageAdapterFactory.get() to always return adapter (a fresh MemoryStorageAdapter by default), regardless of the requested target. Returns { adapter, restore() } |
resetStorageState() | Clears the shared useStorage()/engine instance cache (Vue and React alike) and StorageAdapterFactory's per-target singletons |
seedEnvelope(adapter, key, value, opts?) | Writes a raw envelope directly, for arranging state without a live useStorage() instance |
seedExpiredEnvelope(adapter, key, value, opts?) | seedEnvelope() with exp defaulted to a timestamp already in the past |
flushAsync(ms?) | await-able delay (default 10ms) for letting pending writes/debounce/throttle/dynamic imports settle |
MemoryStorageAdapter, StorageAdapterFactory | Re-exported for convenience |