useStorage
Reactive storage with automatic serialization, caching, and SSR-safe operations.
Installation
Install the Storage plugin in your app’s entry point:
import { createApp } from 'vue'
import { createStoragePlugin } from '@vuetify/v0'
import App from './App.vue'
const app = createApp(App)
app.use(createStoragePlugin())
app.mount('#app')Usage
Once the plugin is installed, use the useStorage composable in any component:
<script setup lang="ts">
import { useStorage } from '@vuetify/v0'
const storage = useStorage()
// Get a reactive ref for a storage key
const username = storage.get('username', 'Guest')
// Update the value (automatically persists to storage)
function updateUsername(name: string) {
username.value = name
}
</script>
<template>
<div>
<h1>Welcome, {{ username }}!</h1>
<input v-model="username" placeholder="Enter your name" />
</div>
</template>Adapters
Adapters let you swap the underlying storage backend without changing your application code.
| Adapter | Import | Description |
|---|---|---|
localStorage | — | Browser localStorage (default in browser) |
sessionStorage | — | Browser sessionStorage |
MemoryStorageAdapter | @vuetify/v0/storage/adapters/memory | In-memory storage (default in SSR) |
Architecture
useStorage uses the plugin pattern with storage adapters:
Reactivity
The get() method returns reactive refs that sync with storage automatically.
| Property | Reactive | Notes |
|---|---|---|
get() return value | Returns Ref<T> synced with storage | |
has() | Returns boolean — checks if key exists (TTL-aware) |
Auto-persistence Refs returned by get() are watched with { deep: true }. Any changes to the ref value automatically persist to storage.
Empty strings are preserved get() uses nullish coalescing (??) internally, so an empty string '' is a valid stored value — it is never treated as absent or replaced by the default. Only null and undefined trigger the default.
Examples
Theme
has('note'): false
Stored snapshot
name: Guest
theme: system
draft saved: false
Recipes
Standalone Usage
Use createStorage directly without the plugin system for standalone or module-level caching:
import { createStorage } from '@vuetify/v0'
const storage = createStorage({ prefix: 'myapp:' })
storage.set('theme', 'dark')
const theme = storage.get('theme', 'light')
console.log(theme.value) // 'dark'TTL (Time-to-Live)
Set a ttl option (in milliseconds) to automatically expire cached entries. Expired entries return the default value on get() and are removed from storage.
import { createStorage } from '@vuetify/v0'
// Cache expires after 5 minutes
const cache = createStorage({
prefix: 'api-cache:',
ttl: 5 * 60 * 1000,
})
// Store fetched data — automatically timestamped
cache.set('users', await fetchUsers())
// Later reads return the default if expired
const users = cache.get('users', [])How TTL works When ttl is set, values are internally wrapped as { __ttl, __v, __t } with a timestamp. On get(), if the entry is older than the TTL, it is treated as absent and removed from storage. Non-TTL entries stored previously are read normally.
Surface failed writes
Writes are fire-and-forget: a quota error or SecurityError is logged and the in-memory ref stays current, so storage.set cannot reject. The composition point is the adapter — wrap setItem, record the failure, and rethrow so the internal log still fires. Theme/locale persist ride the same plugin instance, so one wrap covers those too. Corrupt stored JSON is a serializer.read failure; wrap that the same way if you need it.
import { createStoragePlugin, IN_BROWSER } from '@vuetify/v0'
import { MemoryStorageAdapter } from '@vuetify/v0/storage/adapters/memory'
import { shallowRef } from 'vue'
const saveError = shallowRef<unknown>(null)
const backend = IN_BROWSER ? localStorage : new MemoryStorageAdapter()
app.use(
createStoragePlugin({
adapter: {
getItem: key => backend.getItem(key),
setItem: (key, value) => {
try {
backend.setItem(key, value)
} catch (error) {
saveError.value = error
throw error
}
},
removeItem: key => backend.removeItem(key),
},
}),
)Watch saveError to drive a notice. Same composition point as a custom logger or notifications adapter — swap the implementation, don’t grow a parallel hook on the factory.
FAQ
No. The ref returned by get(key, default) is watched with { deep: true }, so writing to .value (or binding it with v-model) persists automatically. set() is the explicit alternative when you don’t hold the ref.
get() uses nullish coalescing internally, so '' is treated as a valid stored value — only null and undefined trigger the default. This preserves intentionally-cleared fields.
Pass a ttl (in milliseconds) to createStorage. Entries are timestamped on write; once older than the TTL, get() returns the default and removes the entry from storage.
You don’t, unless you wrap the adapter. setItem throws (quota, SecurityError) inside a deep watcher, so storage.set cannot reject and no error state is exposed. Wrap setItem, record the failure, rethrow so the internal log still fires — see Surface failed writes. Corrupt JSON is serializer.read; wrap that too if you need it.
Pass the backend you want as the adapter option. localStorage is the browser default; sessionStorage scopes values to the tab, and MemoryStorageAdapter (from @vuetify/v0/storage/adapters/memory) keeps them in memory only.
Yes. With no browser storage on the server it falls back to MemoryStorageAdapter, so reads and writes work and the render stays deterministic — values just don’t persist across requests. Pair it with useHydration to coordinate reads with client hydration.