{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidkdpinaseyrq5wxudkjydpwwtsuk4l3ms3i7rhlguzdvar62ovve",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mp2bdaum5vc2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreienfxn2rl2s5hfocaz3jk7qql7scmexbhcimap4qklx5l7xobbvz4"
    },
    "mimeType": "image/webp",
    "size": 85522
  },
  "path": "/kidjoker/stokado-a-zero-dependency-proxy-wrapper-that-makes-browser-storage-feel-like-a-plain-object-3i0",
  "publishedAt": "2026-06-24T15:30:39.000Z",
  "site": "https://dev.to",
  "tags": [
    "webdev",
    "javascript",
    "opensource",
    "showdev",
    "Stokado",
    "localForage",
    "lz-string",
    "useStorage from @vueuse/core",
    "idb-keyval",
    "github.com/KID-joker/stokado",
    "@react-native-async-storage"
  ],
  "textContent": "If you've shipped anything to the browser, you've used `localStorage`. And if you've used it for more than five minutes, you've also written this exact line more times than you'd like to admit:\n\n\n\n    const user = JSON.parse(localStorage.getItem('user') || 'null')\n\n\nThe Web Storage API has aged remarkably well for something so small, but it carries three persistent pain points that every frontend codebase ends up papering over by hand.\n\n**Pain point #1: everything is a string.** `localStorage.setItem('count', 0)` doesn't store the number `0` — it stores the string `\"0\"`. Read it back and `typeof` is `\"string\"`. Booleans become `\"true\"`/`\"false\"`, `Date` objects collapse into ISO strings (if you're lucky) or `\"[object Object]\"` (if you're not), and `undefined` becomes the literal string `\"undefined\"`. So every project grows a thin serialization layer of `JSON.parse`/`JSON.stringify` wrappers, plus a pile of defensive try/catch blocks for the day a malformed value sneaks in.\n\n**Pain point #2: the API is verbose and stringly-typed.** `getItem`, `setItem`, `removeItem` — three method calls and a string key for what is conceptually just reading and writing a property. It reads nothing like the rest of your code.\n\n**Pain point #3: reactivity is broken in the tab you actually care about.** The native `storage` event only fires in _other_ tabs of the same origin. The tab that performed the write never hears about it. So if you want to react to your own storage changes — the overwhelmingly common case — the platform gives you nothing.\n\nStokado is a small, zero-dependency library that addresses all three by wrapping any storage object in a `Proxy`. It's framework-agnostic, TypeScript-friendly, and works equally well with `localStorage`, `sessionStorage`, cookies, async backends like localForage, and a handful of mini-program runtimes. This article walks through what it actually does, feature by feature, with runnable code.\n\n##  Quick start\n\n\n    npm install stokado\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage)\n\n    // Write like a plain object\n    storage.token = 'abc'\n\n    // Read like a plain object\n    console.log(storage.token) // 'abc'\n\n    // Delete with the delete operator\n    delete storage.token\n\n\n`createProxyStorage` takes any storage-like object and returns a proxy that behaves like a plain object on the surface, while doing serialization, change notification, expiration, and quota bookkeeping underneath. That's the whole mental model. Everything below is a refinement of it.\n\n##  Type-safe serialization\n\nThe single most repetitive chore with native storage is converting values to and from strings. Stokado handles it transparently — and crucially, it preserves the _type_ , not just the shape.\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage)\n\n    storage.count = 0\n    storage.enabled = false\n    storage.missing = null\n    storage.createdAt = new Date('2024-01-01T00:00:00Z')\n    storage.pattern = /^\\d+$/g\n    storage.profile = { name: 'Ada', roles: ['admin'] }\n\n    console.log(typeof storage.count) // 'number'  (not \"0\")\n    console.log(storage.enabled === false) // true     (not \"false\")\n    console.log(storage.missing) // null     (not \"null\")\n    console.log(storage.createdAt instanceof Date) // true\n    console.log(storage.pattern.test('42')) // true\n    console.log(storage.profile.roles[0]) // 'admin'\n\n\nThe trap that catches naive `JSON.parse` wrappers is the falsy-but-valid value: `storage.count = 0` round-trips as the number `0`, not the string `\"0\"` and not `undefined`. The same holds for `false` and `null`. Stokado records the original type tag at write time and reconstructs the matching JavaScript value at read time.\n\nThe supported type set is deliberately broad. In addition to the obvious `string`, `number`, `boolean`, `null`, `undefined`, `object`, and `array`, stokado round-trips `BigInt`, `Date`, `RegExp`, `URL`, `Set`, `Map`, and even `Function`:\n\n\n\n    storage.id = 9007199254740993n\n    storage.tags = new Set(['a', 'b', 'a'])\n    storage.lookup = new Map([['x', 1]])\n    storage.api = new URL('https://example.com/v1')\n    storage.greet = function (name) {\n      return `hi ${name}`\n    }\n\n    console.log(typeof storage.id) // 'bigint'\n    console.log(storage.tags instanceof Set) // true\n    console.log(storage.lookup.get('x')) // 1\n    console.log(storage.api.host) // 'example.com'\n    console.log(storage.greet('Ada')) // 'hi Ada'\n\n\nA note on `Function`: stokado serializes it via `toString()` and rebuilds it with the `Function` constructor on read. That's genuinely useful for storing small configuration callbacks, but be aware it relies on dynamic evaluation and loses closure scope — treat it as a convenience for trusted, self-contained functions rather than a general serialization mechanism.\n\nIf you ever need the raw conversion outside of a proxy, stokado exports the underlying `encode` and `decode` functions directly:\n\n\n\n    import { decode, encode } from 'stokado'\n\n\n##  Same-tab reactive subscriptions\n\nThis is where stokado earns its keep. You can subscribe to changes on a specific key, and — unlike the native `storage` event — the callback fires in the _same_ tab that made the change.\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage)\n\n    storage.on('token', (newValue, oldValue) => {\n      console.log(`token changed: ${oldValue} -> ${newValue}`)\n    })\n\n    storage.token = 'abc' // logs: token changed: undefined -> abc\n    storage.token = 'xyz' // logs: token changed: abc -> xyz\n\n\nEvery listener receives `(newValue, oldValue)`, both already deserialized to their proper types. There are three methods:\n\n  * `on(key, fn)` — subscribe to all future changes for `key`.\n  * `once(key, fn)` — fire exactly once, then auto-unsubscribe.\n  * `off(key?, fn?)` — unsubscribe. Pass a key and function to remove one listener, pass just a key to remove all listeners for that key, or pass nothing to clear everything.\n\n\n\n\n    function handler(n, o) {\n      console.log('once:', n, o)\n    }\n\n    storage.once('session', handler)\n    storage.session = 'first' // fires once\n    storage.session = 'second' // silent — already unsubscribed\n\n    storage.on('cart', updateBadge)\n    storage.off('cart', updateBadge) // remove a single listener\n\n\nSubscriptions even reach into nested properties using dot paths, which is handy when you store structured objects:\n\n\n\n    storage.on('profile.name', (n, o) => console.log(`name: ${o} -> ${n}`))\n\n    storage.profile = { name: 'Ada' }\n    storage.profile.name = 'Grace' // logs: name: Ada -> Grace\n\n\nBecause this works in the same tab, you can use storage as a lightweight intra-app event bus — for example, keeping a header avatar in sync with a settings page without reaching for a global store.\n\n##  Expiration and disposable values\n\nNative storage has no concept of a TTL. Stokado adds two complementary lifetime controls through the third argument of `setItem`.\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage)\n\n    // Expires at a specific time (timestamp or Date)\n    storage.setItem('session', 'token-123', {\n      expires: Date.now() + 60_000, // gone in 60 seconds\n    })\n\n    // Disposable: self-destructs after the first read\n    storage.setItem('flash', 'You just signed in!', {\n      disposable: true,\n    })\n\n    console.log(storage.flash) // 'You just signed in!'\n    console.log(storage.flash) // undefined — consumed on first read\n\n\n`expires` accepts a timestamp, a date string, or a `Date` instance. Once the moment passes, the value is treated as gone — reading it returns `undefined` and the entry is cleaned up. `disposable` is a one-shot read: perfect for flash messages, single-use tokens, or any \"show this exactly once\" value.\n\nThe two can be combined, and there's a small family of helpers for managing lifetimes after the fact:\n\n\n\n    // Set / inspect / clear an expiry on an existing key\n    storage.setExpires('session', new Date('2030-01-01'))\n    console.log(storage.getExpires('session')) // Date instance\n    storage.removeExpires('session')\n\n    // Mark an existing key as disposable\n    storage.setDisposable('flash')\n\n    // Inspect the full option set for a key\n    console.log(storage.getOptions('session'))\n    // e.g. { expires: <Date>, disposable: true } — or null if no options set\n\n\n`getOptions` returns whatever lifetime metadata is attached to a key (`expires`, `disposable`, or both), or `null` if the key is a plain value. It's a clean way to introspect state without guessing.\n\n##  Cross-tab synchronization\n\nSame-tab reactivity is great, but sometimes you genuinely want every open tab to agree. Stokado can broadcast changes across tabs of the same origin using the `BroadcastChannel` API.\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage, {\n      broadcast: true,\n      channel: 'app-storage',\n    })\n\n    // In any tab:\n    storage.on('theme', (next) => {\n      document.documentElement.dataset.theme = next\n    })\n\n    // In another tab:\n    storage.theme = 'dark' // every tab's listener fires\n\n\nWith `broadcast` enabled, a write in one tab propagates over the named channel and triggers your subscriptions in the others — so your same-tab `on`/`once` handlers and your cross-tab handlers share one unified API. The `channel` option lets you scope the broadcast so unrelated proxies don't talk over each other.\n\n##  Quota alerts\n\nWeb Storage throws a `QuotaExceededError` when you exceed the (browser-dependent, typically ~5 MB) limit, and by then it's too late. Stokado lets you set a soft quota and get a callback _before_ a write lands.\n\n\n\n    import { createProxyStorage, MB } from 'stokado'\n\n    const storage = createProxyStorage(localStorage, {\n      quota: 4 * MB,\n      onQuotaExceeded(info) {\n        console.warn(\n          `Quota hit on \"${info.key}\": ${info.current}/${info.limit} bytes`,\n        )\n        // Return false to block this write entirely\n        return false\n      },\n    })\n\n\nThe callback receives a `QuotaInfo` object with `{ current, limit, key, value }` — the projected total size, your configured limit, and the key/value being written. The return value controls the write:\n\n  * Return `false` to **reject** the write (the value is not stored).\n  * Return anything else (or nothing) to **allow** it.\n\n\n\nThe handler may also be `async`, which is useful when you want to evict old entries or ask the user before deciding:\n\n\n\n    const storage = createProxyStorage(localStorage, {\n      quota: 4 * MB,\n      async onQuotaExceeded(info) {\n        const proceed = await confirmWithUser(info)\n        return proceed // false blocks, true allows\n      },\n    })\n\n\nFor proactive monitoring, `getUsage()` reports current consumption against the limit at any time:\n\n\n\n    const { current, limit } = storage.getUsage()\n    console.log(`Using ${current} of ${limit} bytes`)\n\n\nStokado measures byte size accurately (via `Blob`), so the numbers reflect real UTF-8 encoded sizes rather than naive string lengths. The exported `KB` and `MB` constants are just `1024` and `1024 * 1024` to keep your quota declarations readable.\n\n##  Async backends\n\n`localStorage` caps out around 5 MB and is synchronous. For larger or non-blocking storage, localForage is the go-to — it transparently uses IndexedDB and exposes a Promise-based API. Stokado proxies it just as happily, returning an `AsyncProxyStorage` whose methods return Promises.\n\n\n\n    import localforage from 'localforage'\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localforage)\n\n    // Wait for the backend to be ready before first use\n    await storage.ready\n\n    await storage.setItem('test', 'hello stokado')\n    console.log(await storage.test) // 'hello stokado'\n\n    await storage.removeItem('test')\n    console.log(await storage.test) // undefined\n\n\nStokado auto-detects that the backend is asynchronous and adapts its surface accordingly. A few differences from the sync case to keep in mind:\n\n  * Reads and writes resolve to Promises, so you `await` them (`await storage.test`, `await storage.setItem(...)`).\n  * `ready` is a Promise you can await to ensure the backend has finished initializing.\n  * `length()` is a method that returns a Promise, not a synchronous property.\n  * `getUsage()` returns a Promise.\n\n\n\nEverything else — type preservation, subscriptions, expiration, disposable values — works identically. The same `setItem('key', value, { expires, disposable })`, the same `on`/`once`/`off`, just with `await` where the platform forces it.\n\n##  Multi-platform presets\n\nBrowser `localStorage` is only one of many key-value stores in the JavaScript world. Stokado ships seven ready-made presets as sub-path imports, each adapting a foreign storage API to the shape `createProxyStorage` expects. Import only the one you need — they're tree-shakeable.\n\n**Cookies** — a `Storage`-like view over `document.cookie`:\n\n\n\n    import { createProxyStorage } from 'stokado'\n    import { cookieStorage } from 'stokado/presets/cookie'\n\n    const cookies = createProxyStorage(cookieStorage)\n    cookies.consent = true\n\n\n**WeChat / Douyin / Alipay mini-programs** — each exposes both a sync and an async variant mapping to that platform's storage SDK:\n\n\n\n    import { createProxyStorage } from 'stokado'\n    import { wechatStorage, wechatStorageAsync } from 'stokado/presets/wechat'\n    import { douyinStorage, douyinStorageAsync } from 'stokado/presets/douyin'\n    import { alipayStorage, alipayStorageAsync } from 'stokado/presets/alipay'\n\n    const wx = createProxyStorage(wechatStorage)\n\n\n**uni-app** — the cross-platform mini-program framework, again sync and async:\n\n\n\n    import { createProxyStorage } from 'stokado'\n    import { uniStorage, uniStorageAsync } from 'stokado/presets/uni-app'\n\n    const uni = createProxyStorage(uniStorage)\n\n\n**React Native** — wrap `@react-native-async-storage/async-storage` (or any compatible AsyncStorage) into an async storage-like object:\n\n\n\n    import AsyncStorage from '@react-native-async-storage/async-storage'\n    import { createProxyStorage } from 'stokado'\n    import { createReactNativeStorage } from 'stokado/presets/react-native'\n\n    const storage = createProxyStorage(createReactNativeStorage(AsyncStorage))\n    await storage.ready\n    await storage.setItem('token', 'abc')\n\n\n**Node** — an in-memory store, ideal for tests, SSR, and CLI tools where no browser storage exists:\n\n\n\n    import { createProxyStorage } from 'stokado'\n    import { memoryStorage } from 'stokado/presets/node'\n\n    const storage = createProxyStorage(memoryStorage)\n    storage.cached = { ok: true }\n\n\nBecause the presets normalize everything to the same storage-like interface, the entire stokado feature set — serialization, subscriptions, expiration — works the same across all of them. Write your data layer once, run it on the web, in a mini-program, in React Native, or on the server.\n\n##  How it compares\n\nStokado is not trying to be the only storage tool you'll ever need, and it's worth being honest about where other libraries are the better pick.\n\n  * **If your priority is squeezing data into a smaller footprint** , reach for lz-string. It's purpose-built for compressing strings before they hit storage. Stokado focuses on faithful type round-tripping, not compression.\n  * **If you live entirely in Vue and want storage wired into the reactivity system** , useStorage from @vueuse/core gives you a `ref` that syncs to storage with deep Vue integration. Stokado is framework-agnostic by design and won't give you a Vue `ref` out of the box.\n  * **If you need a large IndexedDB-backed key-value store and nothing else** , idb-keyval is a tiny, focused option. (And if you want stokado's features _on top_ of IndexedDB, recall that it proxies localForage, which sits on IndexedDB.)\n\n\n\nWhere stokado's edge shows is the _combination_ : faithful type preservation **and** same-tab reactivity **and** expiration/disposable values **and** quota awareness, all behind one plain-object interface, with zero dependencies and the same API across browsers, mini-programs, React Native, and Node. No single feature is unique; having all of them composed together, with nothing to install but the package itself, is.\n\n##  Wrap-up\n\nThe Web Storage API gives you a synchronous string-to-string map and not much else. Stokado keeps that simplicity at the surface — `storage.token = 'abc'` — while quietly handling the parts every team reimplements: type-correct serialization, same-tab change subscriptions, TTLs and one-shot values, cross-tab sync, and quota guards. And it does it without dragging in a single dependency, across every JavaScript runtime that has a key-value store.\n\nIf you've been hand-rolling `JSON.parse` wrappers and `storage` event workarounds, give it a try:\n\n\n\n    npm install stokado\n\n\n\n    import { createProxyStorage } from 'stokado'\n\n    const storage = createProxyStorage(localStorage)\n    storage.user = { name: 'Ada', lastSeen: new Date() }\n\n\nThe full source, tests, and API reference live at github.com/KID-joker/stokado.",
  "title": "Stokado: A Zero-Dependency Proxy Wrapper That Makes Browser Storage Feel Like a Plain Object"
}