vue-storage-kit
v0.2.7
Published
Reactive storage for Vue 3 over localStorage, sessionStorage, IndexedDB and cookies — TTL, AES-GCM encryption, migrations and cross-tab sync.
Maintainers
Readme
Storage Kit

Reactive localStorage, sessionStorage, IndexedDB and cookies for Vue 3 (and React) — TTL, AES-GCM encryption, HMAC signing, schema migrations with up/down functions, undo/redo, and cross-tab sync — built on a framework-agnostic core, with Vue and React as thin bindings over it.
Features
- useStorage — unified reactive state over
localStorage,sessionStorage,IndexedDB, or an in-memory store; drop-in replacement for vueuseuseLocalStorage/useSessionStorage. Available for Vue (aRef) and React (auseSyncExternalStore-backed hook) - Schema migrations — versioned data with
up/downmigration chains; runs automatically on version mismatch, writes back the migrated value - TTL — optional time-to-live per key; lazy expiry checked on every read, no timers; manual
cleanExpired()sweep for startup cleanup - AES-GCM encryption — Web Crypto API (
crypto.subtle), key derived from a password via PBKDF2 or supplied as aCryptoKey;reencrypt()/rotateEncryptedKey()to rotate a password without data loss - HMAC signing — lightweight accidental-corruption detection (
sign: { password }) for data that doesn't need to be secret - Undo / redo —
history: nkeeps the last n values in memory;undo()/redo()navigate them - Debounce & throttle —
debouncecoalesces writes after a pause;throttleguarantees a write at most every n ms during continuous changes - Resilient writes — on
QuotaExceededError, sweeps this adapter's own expired-TTL entries and retries once; opt intoevictOnQuotato additionally evict the least-recently-written other keys - Cross-tab sync —
BroadcastChannelwithstorageevent fallback; last-write-wins conflict resolution by timestamp; optional leader election vianavigator.locks - useIndexedDB — promise-based key-value API plus a reactive
useIDBReffor a single key - useCookie — reactive cookies with
expires,sameSite,secure; SSR-aware (H3-backed) when auto-imported inside the Nuxt module - Vue plugin — global prefix, default target/serializer/encrypt, and a global error handler, all applied to every
useStorage()call - Nuxt module — auto-imports all composables; wires up the plugin with runtime config
- Serializer — JSON with round-trip support for
Date,Map,Set, andundefined; bring your own serializer via theSerializer<T>interface - SSR-safe — falls back to in-memory storage when
windowis unavailable;isReadyref lets components show a skeleton until hydration - Devtools — a custom Vue Devtools inspector and timeline over every live
useStorage()instance;/devtoolsentry point, opt-in viasetupDevtools(app) - Testing utilities —
/testingentry point:mockStorage(),resetStorageState(),seedEnvelope()/seedExpiredEnvelope(),flushAsync() - Vue and React as optional peers —
@vue/devtools-apiis the sole required runtime dependency;/crypto,/sync,/compress,/pinia,/devtools,/react,/testingare separate tree-shakeable entry points
When you'd reach for this
localStorage only stores strings, silently fills up without warning, and knows nothing about other open tabs — vue-storage-kit takes those quirks off your hands and adds what plain localStorage never had: entry lifetimes, encryption, cross-tab sync, and format migrations.
- The same app is open in two tabs — A shopper adds something to the cart in one tab and expects to see it in the other without reloading the page. Changes to storage propagate between tabs instantly, not only after a refresh.
- A draft shouldn't stick around forever — A forgotten form draft or a temporary access token shouldn't sit in storage indefinitely — an entry's lifetime expires on its own, and stale data gets cleared out the next time it's read.
- Personal data shouldn't sit in plain text — Anyone can open the browser's dev tools and read storage contents as plain text — sensitive values can be kept encrypted instead of relying on nobody looking.
- An old data shape meets a new app version — After an update, a user's browser might still hold data shaped for a previous version of the app — it gets converted to the current shape automatically, instead of crashing or silently losing data.
Installation
| Environment | Minimum version |
| ------------- | -------------------------------------------------------------------------- |
| Node.js | 18+ |
| Vue | 3.3.0+ (optional — only for the package root / Vue composables) |
| React | 18.0.0+ (optional — only for /react) |
| pinia | 2.0.0+ or 3.0.0+ (optional — only for /pinia) |
| @nuxt/kit | 3.0.0+ (optional — only for the /nuxt module) |
| h3 | 1.0.0+ (optional — only for the Nuxt module's SSR-aware useCookie) |
npm install vue-storage-kitFor Vue (optional peer — only needed if you import from the package root or any Vue-specific composable):
npm install vue@>=3.3For React (vue-storage-kit/react), install React instead — you don't need vue at all:
npm install react@>=18Quick start
<script setup lang="ts">
import { useLocalStorage } from 'vue-storage-kit'
const { value: theme } = useLocalStorage('theme', 'light')
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">Current theme: {{ theme }}</button>
</template>The value is persisted to localStorage and is reactive — changing theme.value writes to storage immediately.
More examples
Vue
A cache that expires itself
TTL stores the expiry right inside the value's envelope — an expired key is removed automatically on the next read, no manual timers.
import { useStorage } from 'vue-storage-kit'
const {
value: otp,
expiry,
remove,
} = useStorage('otp', {
defaultValue: '',
ttl: 5 * 60 * 1000, // 5 minutes
onExpire: () => router.push('/login'),
})
console.log(expiry.value) // Date | null — when this key expiresSynced across tabs
With sync: true, a change in one tab reaches every other open tab instantly — no server, no manual subscriptions.
import { useStorage } from 'vue-storage-kit'
const { value: cart } = useStorage('cart', {
defaultValue: [] as CartItem[],
sync: true,
})
// cart.value stays in sync across every open tab automaticallyStored data upgrades itself on release
A user on an old version opens the app — the migration chain brings their stored data up to the current schema and persists the result immediately, no manual version if/else.
import { useStorage } from 'vue-storage-kit'
interface SettingsV3 {
theme: 'light' | 'dark'
locale: string
}
const { value: settings } = useStorage<SettingsV3>('settings', {
defaultValue: { theme: 'light', locale: 'en' },
version: 3,
migrations: [
{ version: 2, up: (d: any) => ({ ...d, theme: d.darkMode ? 'dark' : 'light' }) },
{ version: 3, up: (d: any) => ({ ...d, locale: d.lang ?? 'en' }) },
],
onMigrate: (from, to) => console.log(`Migrated settings ${from} → ${to}`),
})
// A v1 user with { darkMode: true } in storage reads
// { darkMode: true, theme: 'dark', locale: 'en' } — the migration chain
// runs and persists automatically, on the very first read.React
The same engine, as a React hook
The vue-storage-kit/react entry point exports useStorage() with the same options (TTL, migrations, sync, and the rest) as the Vue composable — just a different call shape, for React.
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>
}Documentation & links
- 📖 Full documentation: npm.vuecraft.ru/en/packages/vue-storage-kit
- 🌐 VueCraft: vuecraft.ru/en
- 👤 Author: macrulez.ru/en
- 💻 GitHub: macrulezru/vue-storage-kit
- 📦 NPM: vue-storage-kit
- 🐛 Issues: github.com/macrulezru/vue-storage-kit/issues
License
MIT
💖 Support the project
Open source takes time and effort. If this library saves you time or brings value, consider supporting further development.
Thank you for being part of this journey. ❤️
