@pyreon/storage — API Reference
Generated from
storage'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see storage.
Signal-backed persistence for Pyreon. Every stored value is a reactive signal that persists writes automatically to the underlying storage backend. useStorage (localStorage, cross-tab synced), useSessionStorage, useCookie (SSR-readable, configurable expiry), useIndexedDB (large data, debounced writes), and useMemoryStorage (ephemeral, SSR-safe). All hooks return StorageSignal<T> which extends Signal<T> with .remove(). createStorage(backend) enables custom backends (encrypted, remote, etc.). SSR-safe — browser-API hooks return the default value on the server.
Features
useStorage — localStorage-backed with cross-tab sync via storage events
useSessionStorage — per-tab ephemeral storage
useCookie — SSR-readable with configurable path, maxAge, sameSite
useIndexedDB — large data with debounced async writes
useMemoryStorage — in-memory fallback, SSR-safe
createStorage(backend) — factory for custom storage backends
StorageSignal<T> extends Signal<T> with .remove()
version + migrate — persisted-schema migration for evolving stored shapes (all backends, cross-tab-aware)
onError fires on WRITE failures too (quota-exceeded / blocked storage), not just deserialize — no more silent swallow
Complete example
A full, end-to-end usage of the package:
import { useStorage, useSessionStorage, useCookie, useIndexedDB, useMemoryStorage, createStorage } from '@pyreon/storage'
// localStorage — persistent, cross-tab synced via storage events:
const theme = useStorage('theme', 'light')
theme() // 'light' — reactive signal read
theme.set('dark') // updates signal + writes to localStorage
theme.remove() // removes from storage, resets to default
// sessionStorage — per-tab, cleared on tab close:
const filter = useSessionStorage('filter', { query: '', page: 1 })
filter.set({ query: 'search', page: 2 })
// Cookie — SSR-readable, configurable expiry:
const locale = useCookie('locale', 'en', {
maxAge: 365 * 86400, // 1 year
path: '/',
sameSite: 'lax',
})
// IndexedDB — large data, debounced writes:
const draft = useIndexedDB('article-draft', {
title: '',
body: '',
tags: [] as string[],
})
// Memory storage — ephemeral, SSR-safe fallback:
const temp = useMemoryStorage('temp-data', { count: 0 })
// Custom backend — encrypted, remote, etc.:
const encryptedBackend = {
get: (key: string) => decrypt(localStorage.getItem(key)),
set: (key: string, value: string) => localStorage.setItem(key, encrypt(value)),
remove: (key: string) => localStorage.removeItem(key),
}
const useEncrypted = createStorage(encryptedBackend)
const secret = useEncrypted('api-key', '')Exports
| Symbol | Kind | Summary |
|---|---|---|
useStorage | hook | Create a reactive signal backed by localStorage. |
useCookie | hook | Reactive signal backed by browser cookies. |
useSessionStorage | hook | Per-tab ephemeral reactive storage. |
useMemoryStorage | hook | In-memory reactive signal that mimics the storage hook shape — useful as an SSR-safe fallback or in environments without |
setCookieSource | function | Tell useCookie how to read cookies during SSR. |
useIndexedDB | hook | Reactive signal backed by IndexedDB for large data. |
createStorage | function | Factory for custom storage backends. |
removeStorage | function | Imperatively remove a single key from storage and RESET its signal to the default value. |
clearStorage | function | Imperatively clear all storage entries MANAGED by @pyreon/storage for one backend (default local) or all backends. |
API
useStorage hook
<T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T>Create a reactive signal backed by localStorage. Reads the stored value on creation (falling back to defaultValue if absent or on SSR), writes on every .set(), and syncs across browser tabs via storage events. Returns StorageSignal<T> which extends Signal<T> with .remove() to delete the key and reset to default. Serialization defaults to JSON; provide custom serializer/deserializer in options for non-JSON types.
Example
const theme = useStorage('theme', 'light')
theme() // 'light'
theme.set('dark') // persists + cross-tab sync
theme.remove() // delete from storage, reset to defaultCommon mistakes
Expecting cross-tab sync with
useSessionStorage— onlyuseStorage(localStorage) fires storage events across tabsStoring non-serializable values (functions, class instances) without custom
serializer/deserializer— JSON.stringify drops them silentlyReading
.remove()return value — it returns void, not the removed valueEvolving the stored shape without
version+migrate— a user with the OLD shape on disk loads it as-is (oronError/default if it no longer parses). Bumpversionand providemigrateto transform the old shape; a pre-versioning value is migrated as version0.Assuming a
.set()that exceeds quota throws — the in-memory signal always updates; thesetItemfailure is routed toonError(a notification) instead of throwing. ProvideonErrorto surface quota problems to the user.
See also: useSessionStorage · useCookie · useIndexedDB · createStorage
useCookie hook
<T>(key: string, defaultValue: T, options?: CookieOptions) => StorageSignal<T>Reactive signal backed by browser cookies. SSR-readable — on the server, reads from the request cookie header via setCookieSource(). Options include maxAge, path, domain, sameSite, secure. Same StorageSignal<T> return type as other hooks.
Example
const locale = useCookie('locale', 'en', { maxAge: 365 * 86400, path: '/' })
locale.set('fr')Common mistakes
Forgetting
setCookieSource(req.headers.get("cookie"))on SSR — without it the server-side render starts fromdefaultValue, not the user's actual cookie; the page flashes the wrong locale/theme until client-side hydration corrects it.Omitting
sameSitefor auth-style cookies — the browser default has tightened across vendors. Be explicit:sameSite: "lax"(default for nav) or"strict"(login cookies) or"none"(cross-origin embeds withsecure: true).Setting
maxAgein milliseconds — it's in SECONDS (matches the HTTP spec).maxAge: 86400is one DAY, not one minute.Storing > 4KB in a cookie — browsers enforce a ~4KB per-cookie limit. Reach for
useIndexedDBfor large values; cookies are for small server-readable state.
See also: useStorage · setCookieSource · useSessionStorage
useSessionStorage hook
<T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T>Per-tab ephemeral reactive storage. Same shape as useStorage but writes go to sessionStorage instead of localStorage — cleared when the tab closes. NO cross-tab sync (browsers do not fire storage events for sessionStorage). Useful for per-visit filter state, unsaved form drafts that shouldn't survive tab close, and any state that should NOT outlive the current browsing session.
Example
const filter = useSessionStorage('list-filter', { query: '', page: 1 })
filter.set({ query: 'pyreon', page: 1 })
// → persists for the tab's lifetime; gone on closeCommon mistakes
Expecting cross-tab sync — sessionStorage is per-tab by spec. Two tabs on the same page each have their own independent sessionStorage. For shared state across tabs, use
useStorage(localStorage).Treating sessionStorage as "private" — same JavaScript-readable shape as localStorage; do not store secrets there.
See also: useStorage · useMemoryStorage
useMemoryStorage hook
<T>(key: string, defaultValue: T) => StorageSignal<T>In-memory reactive signal that mimics the storage hook shape — useful as an SSR-safe fallback or in environments without localStorage/sessionStorage (sandbox iframes, web workers without DOM, some embedded WebViews). Same StorageSignal<T> shape with .remove(). Values are lost on page reload; no persistence.
Example
const draft = useMemoryStorage('draft-id-42', '')
draft.set('typing...')
// → reactive, but cleared on reloadCommon mistakes
Reaching for useMemoryStorage when a plain
signal()would do — if you don't need the StorageSignal.remove()shape or the cross-storage-backend interchangeability, a plainsignal(defaultValue)is simpler.Expecting persistence — values vanish on reload by design. If persistence is needed, swap to
useStorage/useSessionStorage/useIndexedDB.
See also: useStorage · useSessionStorage
setCookieSource function
setCookieSource(source: string | (() => string) | null) => voidTell useCookie how to read cookies during SSR. Pass the raw cookie header string, an accessor () => string returning it, or null to clear. The source is a single module-level slot: a bare STRING is shared across concurrent requests (safe only when rendering is serialized per process), so on a server handling concurrent requests pass an ACCESSOR bound to your per-request context (e.g. reading the current request's Cookie header out of runWithRequestContext's AsyncLocalStorage) — the accessor is evaluated LAZILY at each cookie read, so each request resolves its own cookies without this module holding per-request state.
Example
import { setCookieSource } from '@pyreon/storage'
// Inside an SSR handler:
setCookieSource(request.headers.get('cookie') ?? '')
const html = await renderToString(<App />)Common mistakes
Forgetting to call setCookieSource on SSR —
useCookiefalls back todefaultValueon every request, ignoring the user's real cookie state. The page hydrates correctly on the client but flashes the default first.Passing a bare STRING source on a CONCURRENTLY-rendering server — the source is one module-level slot, so request A's string can leak into request B's render. Pass an accessor
() => currentRequest().cookieHeaderbound to your per-request context (it's evaluated lazily at read time) so each request resolves its own cookies.Passing a stale cookie source after redirect or login — the source is captured once; re-call after any operation that should change the cookie set.
Calling setCookieSource(null) too early — call it at request CLEANUP (after the response is sent), not before render. Cleaning up mid-render erases the source from later loaders.
See also: useCookie
useIndexedDB hook
<T>(key: string, defaultValue: T, options?: IndexedDBOptions) => StorageSignal<T>Reactive signal backed by IndexedDB for large data. Writes are debounced to avoid excessive I/O. The signal initializes with defaultValue synchronously and hydrates from IndexedDB asynchronously — the value updates reactively once the read completes. Silent init error logging in dev mode.
Example
const draft = useIndexedDB('article-draft', { title: '', body: '' })
draft.set({ title: 'New Article', body: 'Content...' })Common mistakes
Reading the signal in render and expecting the persisted value on FIRST render — IDB initialization is async. The signal starts at
defaultValue, then the persisted value flows in on the next tick. UIs that need the persisted value before paint should pair with a synchronous fallback (e.g.useStoragefor a small marker).Storing huge blobs without considering quota — IDB has per-origin quotas (~50% of free disk, browser-dependent). Bumping into the quota throws on
setItemasync; handle with try/catch around.set()if the write may exceed.Expecting cross-tab sync — IndexedDB does NOT fire storage events. Two tabs writing to the same key will overwrite each other silently. Use
BroadcastChannelalongside if multi-tab consistency matters.Setting the value rapidly in a loop — writes are debounced but unbounded loop assignments still queue. Throttle at the caller for high-frequency mutations.
See also: useStorage · useMemoryStorage
createStorage function
(backend: StorageBackend | AsyncStorageBackend) => <T>(key: string, defaultValue: T, options?: StorageOptions<T>) => StorageSignal<T>Factory for custom storage backends. Pass an object with get, set, remove methods (sync or async) and receive a hook function with the same signature as useStorage. Use for encrypted storage, remote backends, or any custom persistence layer.
Example
const useEncrypted = createStorage({
get: (key) => decrypt(localStorage.getItem(key)),
set: (key, value) => localStorage.setItem(key, encrypt(value)),
remove: (key) => localStorage.removeItem(key),
})
const secret = useEncrypted('api-key', '')Common mistakes
Returning
undefinedfrom the backendgetwhen the key is absent — returnnull(matches the localStorage / sessionStorage contract).undefinedmay be JSON-serialized as the literal string"undefined"by some serialize-deserialize pipelines.Throwing synchronously from setItem — backend errors should be either logged + swallowed (graceful degradation, the signal still updates) OR propagated via a rejected Promise for async backends. A thrown error breaks the calling
.set()and leaves the in-memory signal in a state inconsistent with the backend.Forgetting that the backend must implement ALL three (
get,set,remove) —.remove()calls the backendremove, and omitting it makes the hook crash on cleanup paths.
See also: useStorage
removeStorage function
removeStorage(key: string, options?: { type?: 'local' | 'session' | 'cookie' | 'indexeddb' }) => voidImperatively remove a single key from storage and RESET its signal to the default value. Default backend is local; pass { type: "session" | "cookie" | "indexeddb" } for the others. Works whether or not a useStorage-family signal is currently registered for the key: a registered key routes through signal.remove() (reactive reset + storage delete), an unregistered key clears the raw storage directly (removeItem / cookie max-age=0).
Example
import { removeStorage } from '@pyreon/storage'
removeStorage('theme') // localStorage
removeStorage('step', { type: 'session' }) // sessionStorage
removeStorage('locale', { type: 'cookie' }) // deletes the cookieCommon mistakes
Expecting the signal to read
undefinedafterward —removeStorage(key)RESETS the signal to itsuseStorageDEFAULT (auseStorage("theme", "light")reads"light"again, notundefined).Omitting
{ type }for a non-local backend — it defaults tolocal; useremoveStorage("locale", { type: "cookie" })for a cookie, etc.Mixing up the arg shape with
clearStorage—removeStoragetakes the backend in an OPTIONS object ({ type }), whileclearStoragetakes it POSITIONALLY (clearStorage("session")). The two are asymmetric.
See also: clearStorage · useStorage
clearStorage function
clearStorage(type?: 'local' | 'session' | 'cookie' | 'indexeddb' | 'all') => voidImperatively clear all storage entries MANAGED by @pyreon/storage for one backend (default local) or all backends. Each managed entry's signal is RESET to its default value (reactive — components reading it re-render to the default). It only touches keys @pyreon/storage tracks (created via useStorage / useSessionStorage / useCookie / useIndexedDB); unmanaged keys in the same backend are LEFT ALONE — it is NOT localStorage.clear().
Example
import { clearStorage } from '@pyreon/storage'
clearStorage() // all MANAGED localStorage entries
clearStorage('session') // all managed sessionStorage entries
clearStorage('all') // every backendCommon mistakes
Expecting
clearStorage()to wipe the whole backend likelocalStorage.clear()— it only clears keys MANAGED by @pyreon/storage (registered via the hooks); unmanaged keys are untouched. Call the nativelocalStorage.clear()for a full wipe.Assuming it clears every backend — it defaults to
localonly; pass"all"for everything, or the specific backend name ("session"/"cookie"/"indexeddb").Expecting cleared signals to read
undefined— each entry RESETS to itsuseStorageDEFAULT value (reactive), notundefined.
See also: removeStorage · useStorage
Package-level notes
SSR safety: Browser-backed hooks (
useStorage,useSessionStorage,useIndexedDB) return the default value on the server.useCookieis SSR-readable viasetCookieSource()which reads from the request headers.
Cross-tab sync: Only
useStorage(localStorage) syncs across tabs viastorageevents.useSessionStorageis per-tab. Cookies and IndexedDB have no built-in cross-tab notification.
IndexedDB async init: The IndexedDB hook initializes synchronously with the default value, then hydrates asynchronously. Components reading the value in their first render see the default — the value updates reactively once the IDB read completes. Init/read/write failures are routed to
onError.
Versioned migration: Set
versionto store values inside a small JSON envelope carrying the schema version; a later load with a HIGHERversionrunsmigrate(oldValue, fromVersion)to transform the old shape. A legacy value written before versioning (no envelope) is treated as version0. Migration is applied on read AND on cross-tab sync (the entry's options travel with it), so a tab holding the old shape upgrades when a newer tab writes.