Skip to content

Integrations

Vue plugin

Install the plugin to configure a global key prefix, default target, and error handler.

ts
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.

OptionTypeDescription
prefixstringPrepended 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
defaultTargetStorageTargetUsed when a call doesn't pass target itself; an explicit target (including the one baked into useLocalStorage/useSessionStorage) always wins
defaultSerializerSerializer<unknown>Fallback used when a call doesn't pass its own serializer
defaultEncryptEncryptOptionsWith 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) => voidCalled 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, and error events 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.

ts
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:

ts
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.

ts
// 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:

ts
useStorage()
useLocalStorage()
useSessionStorage()
useIndexedDB()
useIDBRef()
useCookie()

SSR caveat: the StorageEngine instance cache (keyed by target: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 to defaultValue server-side (there's no window in 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.

tsx
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

VueReact
Returns{ value: Ref<T>, ... } — assign value.value = x to write{ value: T, setValue, ... } — call setValue(x) or setValue(prev => next) to write
Shared instanceTwo Vue components with the same key+target share one RefTwo React components with the same key+target share one underlying engine (via useSyncExternalStore), but each gets its own snapshot
Reacting to a changed keyNot supported — same as the Vue sideNot 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).

ts
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')
})
ExportDescription
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, StorageAdapterFactoryRe-exported for convenience