Skip to content

Resilience & Sync ​

Resilience & performance ​

Debounce and throttle ​

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

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

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

ts
const { value: cart } = useStorage('cart', {
  defaultValue: [] as CartItem[],
  sync: true,
})
// cart.value stays in sync across all tabs automatically

SyncOptions ​

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.

ts
const { value: sharedState } = useStorage('shared', {
  defaultValue: { count: 0 },
  sync: { channel: 'app-sync', leader: true, debounce: 100 },
})

Use TabSync directly ​

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

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

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