useLocalStorage
zyeon/use-local-storageFreeHook
An SSR-safe localStorage-backed state hook, kept in sync across same-tab instances and other tabs.
Install it
npx shadcn@latest add https://ui.zyeon.ai/r/use-local-storage.jsonSource
1"use client"23import * as React from "react"45/**6 * Custom event name used to sync instances inside one tab. The native "storage" event7 * only fires in *other* tabs/windows — the document that performed the write never8 * hears its own storage event — so multiple `useLocalStorage(key)` instances in one9 * tab notify each other through this custom event.10 */11const SYNC_EVENT = "zyeon:local-storage-sync"1213interface SyncEventDetail {14 key: string15}1617function readRawValue(key: string): string | null {18 try {19 return window.localStorage.getItem(key)20 } catch {21 // in private mode, or with storage disabled outright, touching localStorage can itself throw22 return null23 }24}2526function parseOrFallback<T>(raw: string | null, fallback: T): T {27 if (raw === null) return fallback28 try {29 return JSON.parse(raw) as T30 } catch {31 // hand-edited or corrupted storage: fall back to the default, never throw32 return fallback33 }34}3536/**37 * Per-key cache of "the raw string last read + what it parsed to".38 *39 * This is the load-bearing piece of the hook: `useSyncExternalStore` requires that,40 * while the store has not changed, two calls to `getSnapshot` return the `===` same41 * reference. `JSON.parse` allocates a fresh object/array every time, so re-parsing on42 * every `getSnapshot` makes React believe the store changed on every render and43 * re-render forever. Caching the raw string together with its parsed result means we44 * re-parse only when the raw string actually changed, and return the same reference45 * otherwise.46 */47const snapshotCache = new Map<string, { raw: string | null; value: unknown }>()4849function getSnapshotFor<T>(key: string, fallback: T): T {50 const raw = readRawValue(key)51 // key absent: return this instance's own initialValue (held in a per-instance ref,52 // so the reference is stable) and do not write the shared cache — otherwise, with two53 // instances on the same key but different defaults, the one mounted later reads the54 // earlier one's cached default.55 if (raw === null) return fallback56 const cached = snapshotCache.get(key)57 if (cached && cached.raw === raw) {58 return cached.value as T59 }60 const value = parseOrFallback(raw, fallback)61 snapshotCache.set(key, { raw, value })62 return value63}6465function dispatchSync(key: string) {66 window.dispatchEvent(new CustomEvent<SyncEventDetail>(SYNC_EVENT, { detail: { key } }))67}6869/**70 * State backed by localStorage, with the same call signature as `useState`:71// … truncated
Files it writes
More from zyeon
All 893 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| useAsyncuse-async | zyeon | Hooks | Free | no deps | |
| useBatteryuse-battery | zyeon | Hooks | Free | no deps | |
| useBroadcastChanneluse-broadcast-channel | zyeon | Hooks | Free | no deps | |
| useClickOutsideuse-click-outside | zyeon | Hooks | Free | no deps | |
| useClipboardPasteuse-clipboard-paste | zyeon | Hooks | Free | no deps | |
| useControllableStateuse-controllable-state | zyeon | Hooks | Free | no deps | |
| useCookieuse-cookie | zyeon | Hooks | Free | no deps | |
| useCopyToClipboarduse-copy-to-clipboard | zyeon | Hooks | Free | no deps |
