hydrate-storage
Typed, SSR-safe client storage for production web apps. One small core for localStorage, sessionStorage, cookies and in-memory storage, with schema validation, TTL, versioned migrations, pluggable codecs, cross-tab sync and React, Vue and Svelte bindings.
Never throws
Blocked storage, full quota, corrupt data and failed migrations all go through one onError hook.
Hydration-safe
The server and the first client render always see defaultValue, so there is no mismatch.
Tiny
Zero runtime dependencies. About 2.2 kB min+gzip for the core, under 250 B per React or Vue binding.
Validated
Any Standard Schema library works: Zod, Valibot or ArkType. Reads and writes are both checked.
Install
Node 18+ for tooling and SSR; any browser with the Web Storage API. react (18+) and vue (3.3+) are optional peer dependencies, needed only for the matching subpath.
Quick start
Any Standard Schema library works (Zod 3.24+, Valibot, ArkType). Schemas must be synchronous.
Framework guides
React and Next.js
useStorageItem is built on useSyncExternalStore with a server snapshot. In the App Router, hooks live in Client Components; the storage module itself can be imported from Server Components.
Vue and Nuxt
Returns a writable computed. It starts at defaultValue to match server HTML and syncs with storage in onMounted.
Svelte, SvelteKit and Astro
asStore() implements the Svelte store contract. On the server the first emission is defaultValue.
Vanilla and other frameworks
item.get, item.set and item.subscribe(listener) are enough to build a binding. subscribe returns an unsubscribe function and getServerSnapshot is available for SSR-aware stores.
Data lifecycle
Validation
When schema is set it runs on every read and every write. Invalid writes are rejected and set returns false. Invalid stored data is reported and defaultValue is returned; the bad entry stays in storage until the next successful write.
Versioning and migrations
Every entry records the version it was written with (default 1). Migrations are applied lazily on read; the upgraded form is written on the next set.
TTL
ttl is in milliseconds and counted from the last write. Expired entries read as missing and has() is false for them. They are not deleted on read; call storage.purgeExpired(), for example on app start, to reclaim space.
Adapters
In the browser, if Web Storage is blocked or throws (private mode, sandboxed iframes, disabled cookies) the adapter falls back to in-memory storage for the page's lifetime. On the server every built-in adapter except memory is a no-op.
Error handling
No method throws on storage problems. Failures are passed to onError as a StorageError with a code, the full namespaced key and the original cause.
API reference
createStorage(options?)
storage.item(key, options)
Exports
createStorage, createMemoryAdapter, noopAdapter, createWebAdapter, createCookieAdapter, StorageError and the types StorageAdapter, Serializer, Codec, StorageOptions, ItemOptions, StorageItem, Storage, StorageErrorCode, AdapterKind, CookieOptions and StandardSchemaV1. Subpaths: hydrate-storage/react and hydrate-storage/vue, each exporting useStorageItem.
SSR guarantees and limits
SSR and runtime behaviour
The server never reads cookies. If a value must be known during SSR, such as a theme, read it with your framework's cookie API and pass it as the render input.
Limitations
- Everything is synchronous; async backends (IndexedDB, React Native AsyncStorage) and async schemas are not supported.
- Web Storage is limited to a few MB per origin and cookies to about 4 KB per entry.
- Cross-tab sync works for adapters that provide subscribe: local, and custom adapters.
- Expired entries are removed only by purgeExpired() or by overwriting them.
- Do not store secrets or session tokens in JavaScript-readable storage. Use HttpOnly cookies for those.
Source and license
MIT licensed. Full README, changelog and issue tracker are on GitHub (opens in a new tab); the package is on npm (opens in a new tab).