Resilience & Sync
Resilience & performance
Debounce and throttle
// Only the value 500ms after typing stops gets written.
const { value: draft } = useStorage('draft', { defaultValue: '', debounce: 500 })
// Written at most once every 200ms while a slider is dragged, instead of
// waiting for it to stop.
const { value: volume } = useStorage('volume', { defaultValue: 50, throttle: 200 })They're mutually exclusive — if both are set, throttle wins, and a dev-mode console.warn fires once, at construction, to flag it. Whichever is used, a write still pending when the component unmounts is flushed immediately rather than dropped.
Quota-exceeded recovery
On QuotaExceededError, useStorage first sweeps this adapter's own expired-TTL entries (via the same logic as TTLManager.cleanExpired()) and retries once. If that's not enough, opt into evictOnQuota to additionally evict the least-recently-written other keys under the same adapter:
const { value } = useStorage('cache-entry', {
defaultValue: null,
evictOnQuota: { max: 3 }, // evict up to 3 other keys before giving up
})Eviction can only judge the age of plain (unencrypted, uncompressed) envelopes — it leaves encrypted/compressed/signed keys belonging to other useStorage() calls alone, since their age can't be safely inspected without their own keys.
Non-quota write failures (a full disk, a broken adapter) are reported as { type: 'write-failed', key, error } via onError — they're never thrown from inside the internal reactive write, which would otherwise be an unhandled rejection your app never sees.
Undo / redo
const {
value: text,
undo,
redo,
canUndo,
canRedo,
} = useStorage('editor-content', {
defaultValue: '',
history: 20, // keep the last 20 values
})
text.value = 'draft one'
text.value = 'draft two'
undo() // text.value === 'draft one'
redo() // text.value === 'draft two'History lives in memory only — it does not persist across reloads, and isn't itself written to storage. canUndo/canRedo are reactive, so you can disable the corresponding buttons in your UI.
Tab sync
When sync: true, writes to value are broadcast to all other open tabs via BroadcastChannel. Remote updates are applied silently (without writing back to storage). Falls back to window.addEventListener('storage', ...) if BroadcastChannel is unavailable.
const { value: cart } = useStorage('cart', {
defaultValue: [] as CartItem[],
sync: true,
})
// cart.value stays in sync across all tabs automaticallySyncOptions
channel
string · default: 'vue-storage-kit'
BroadcastChannel name.
leader
boolean · default: false
Enable leader election — only one tab writes to storage on conflict.
debounce
number · default: 50
Milliseconds to debounce outgoing broadcasts.
Leader election uses navigator.locks. The leader tab holds a named lock for its lifetime. When the leader closes, another tab automatically acquires the lock and becomes the new leader. When leader: true, conflicts are resolved as last-write-wins by timestamp — on a tie the leader's version is kept.
const { value: sharedState } = useStorage('shared', {
defaultValue: { count: 0 },
sync: { channel: 'app-sync', leader: true, debounce: 100 },
})Use TabSync directly
import { TabSync } from 'vue-storage-kit/sync'
const sync = new TabSync({ channel: 'custom-channel', leader: true })
await sync.start()
sync.subscribe('my-key', (rawValue) => {
console.log('Received from another tab:', rawValue)
})
sync.broadcast('my-key', JSON.stringify({ count: 1 }), Date.now())
sync.stop()useBroadcastChannel
A lower-level, generic pub/sub primitive over BroadcastChannel — not tied to useStorage() at all, unlike the sync option above. Use it for cross-tab messaging that isn't about persisting a storage key (a live chat between tabs, a "someone else made changes" toast, coordinating a shared timer).
function useBroadcastChannel<T>(channelName: string): UseBroadcastChannelReturn<T>isSupported
boolean
Whether BroadcastChannel exists in this environment. When false, post() is a silent no-op and messages never arrive.
lastMessage
ShallowRef<T | null>
The most recently received message.
messages
Ref<T[]>
Every message received so far, oldest first.
post
(message: T) => void
Sends a message to every other tab/window subscribed to the same channelName.
close
() => void
Closes the underlying channel. Runs automatically on scope dispose if called inside an active effect scope.
import { useBroadcastChannel } from 'vue-storage-kit'
interface ChatMessage {
user: string
text: string
}
const { lastMessage, post } = useBroadcastChannel<ChatMessage>('team-chat')
post({ user: 'ada', text: 'Hello from this tab' })
// lastMessage.value updates in every other tab subscribed to 'team-chat'