UNPKG

@technicalshree/use-localstorage

Version:

Feature-rich React hook that keeps state synchronised with localStorage across tabs and sessions.

621 lines (620 loc) 21.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { createCookieStorage: () => createCookieStorage, default: () => index_default, useCookieStorage: () => useCookieStorage, useLocalStorage: () => useLocalStorage, useObjectLocalStorage: () => useObjectLocalStorage, useSyncedStorage: () => useSyncedStorage }); module.exports = __toCommonJS(index_exports); var import_react = require("react"); var SYNC_FLAG = "__useSyncedStorage_v1__"; var UNINITIALIZED = Symbol("useSyncedStorage.uninitialized"); var defaultSerializer = (value) => JSON.stringify(value); var defaultDeserializer = (raw) => JSON.parse(raw); var createMemoryStorage = () => { const store = /* @__PURE__ */ new Map(); return { getItem(key) { return store.has(key) ? store.get(key) : null; }, setItem(key, value) { store.set(key, value); }, removeItem(key) { store.delete(key); } }; }; var MEMORY_STORAGE = createMemoryStorage(); var DEFAULT_COOKIE_PATH = "/"; var normaliseSameSite = (value) => { if (!value) { return void 0; } const normalised = value.toLowerCase(); if (normalised === "none" || normalised === "lax" || normalised === "strict") { return normalised; } return void 0; }; var toUTCString = (input) => { if (input instanceof Date) { return input.toUTCString(); } const parsed = new Date(input); return Number.isNaN(parsed.getTime()) ? new Date(input).toUTCString() : parsed.toUTCString(); }; var serialiseCookie = (key, value, attributes, removal = false) => { var _a; const segments = [ `${encodeURIComponent(key)}=${encodeURIComponent(value)}` ]; const path = (_a = attributes.path) != null ? _a : DEFAULT_COOKIE_PATH; if (path) { segments.push(`Path=${path}`); } if (attributes.domain) { segments.push(`Domain=${attributes.domain}`); } if (attributes.secure) { segments.push("Secure"); } const sameSite = normaliseSameSite(attributes.sameSite); if (sameSite) { segments.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`); } if (removal) { segments.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT"); segments.push("Max-Age=0"); } else { if (attributes.expires) { segments.push(`Expires=${toUTCString(attributes.expires)}`); } if (attributes.maxAge !== void 0) { segments.push(`Max-Age=${Math.max(0, Math.floor(attributes.maxAge))}`); } } return segments.join("; "); }; var parseCookieString = (cookieString, target) => { target.clear(); if (!cookieString) { return; } cookieString.split(";").forEach((entry) => { const part = entry.trim(); if (!part) { return; } const separatorIndex = part.indexOf("="); if (separatorIndex === -1) { return; } const name = decodeURIComponent(part.slice(0, separatorIndex).trim()); const rawValue = part.slice(separatorIndex + 1); target.set(name, decodeURIComponent(rawValue)); }); }; var createCookieStorage = (config = {}) => { var _a, _b; const attributes = { path: DEFAULT_COOKIE_PATH, ...config.attributes }; const getCookieString = ((_a = config.handlers) == null ? void 0 : _a.getCookieString) ? () => { var _a2, _b2, _c; return (_c = (_b2 = (_a2 = config.handlers) == null ? void 0 : _a2.getCookieString) == null ? void 0 : _b2.call(_a2)) != null ? _c : ""; } : () => { var _a2; if (typeof document === "undefined") { return ""; } try { return (_a2 = document.cookie) != null ? _a2 : ""; } catch (error) { return ""; } }; const setCookieString = ((_b = config.handlers) == null ? void 0 : _b.setCookieString) ? (cookie) => { var _a2, _b2; (_b2 = (_a2 = config.handlers) == null ? void 0 : _a2.setCookieString) == null ? void 0 : _b2.call(_a2, cookie); } : (cookie) => { if (typeof document === "undefined") { return; } try { document.cookie = cookie; } catch (error) { } }; const state = /* @__PURE__ */ new Map(); let initialised = false; let dirty = false; let lastSnapshot = null; const initialise = () => { if (initialised) { return; } const snapshot = getCookieString(); parseCookieString(snapshot != null ? snapshot : "", state); lastSnapshot = snapshot != null ? snapshot : ""; initialised = true; }; const refreshFromSource = () => { if (dirty) { return; } const snapshot = getCookieString(); if (snapshot === void 0 || snapshot === lastSnapshot) { return; } parseCookieString(snapshot, state); lastSnapshot = snapshot; }; const markDirty = () => { dirty = true; }; const clearDirty = () => { dirty = false; const snapshot = getCookieString(); if (snapshot === void 0) { return; } if (snapshot !== lastSnapshot) { parseCookieString(snapshot, state); lastSnapshot = snapshot; } }; return { getItem(key) { initialise(); refreshFromSource(); return state.has(key) ? state.get(key) : null; }, setItem(key, value) { initialise(); state.set(key, value); markDirty(); setCookieString(serialiseCookie(key, value, attributes)); clearDirty(); }, removeItem(key) { initialise(); state.delete(key); markDirty(); setCookieString(serialiseCookie(key, "", attributes, true)); clearDirty(); } }; }; var resolveInitial = (value) => value instanceof Function ? value() : value; var isEnvelope = (payload) => { return !!payload && typeof payload === "object" && SYNC_FLAG in payload; }; var interpretStoredValue = (raw, options, reportError) => { var _a, _b; if (raw === null) { return { kind: "empty" }; } let parsed; try { parsed = JSON.parse(raw); } catch (error) { reportError(error, "read"); return { kind: "error" }; } if (isEnvelope(parsed)) { const envelope = parsed; const meta = (_a = envelope.meta) != null ? _a : null; if ((meta == null ? void 0 : meta.expiresAt) !== void 0 && meta.expiresAt <= Date.now()) { return { kind: "expired", meta }; } const deserializer = (_b = options == null ? void 0 : options.deserializer) != null ? _b : defaultDeserializer; try { const value = deserializer(envelope.data); return { kind: "ok", value, meta }; } catch (error) { reportError(error, "deserialize"); return { kind: "error" }; } } return { kind: "legacy", value: parsed, meta: null }; }; var isStorageLike = (candidate) => { if (!candidate || typeof candidate !== "object") { return false; } return ["getItem", "setItem", "removeItem"].every( (key) => typeof candidate[key] === "function" ); }; var resolveStorage = (options) => { var _a; const canUseDom = typeof window !== "undefined"; const source = (_a = options == null ? void 0 : options.storage) != null ? _a : (options == null ? void 0 : options.cookie) ? "cookie" : void 0; if (!source || source === "local") { if (canUseDom && typeof window !== "undefined" && window.localStorage) { return { storage: window.localStorage, listenToStorageEvent: true }; } return { storage: MEMORY_STORAGE, listenToStorageEvent: false }; } if (source === "session") { if (canUseDom && typeof window !== "undefined" && window.sessionStorage) { return { storage: window.sessionStorage, listenToStorageEvent: true }; } return { storage: MEMORY_STORAGE, listenToStorageEvent: false }; } if (source === "memory") { return { storage: MEMORY_STORAGE, listenToStorageEvent: false }; } if (source === "cookie") { return { storage: createCookieStorage(options == null ? void 0 : options.cookie), listenToStorageEvent: false }; } if (isStorageLike(source)) { const listenToStorageEvent = canUseDom && (source === window.localStorage || source === window.sessionStorage); return { storage: source, listenToStorageEvent }; } return { storage: MEMORY_STORAGE, listenToStorageEvent: false }; }; function useSyncedStorage(key, initialValue, options) { const optionsRef = (0, import_react.useRef)(options); (0, import_react.useEffect)(() => { optionsRef.current = options; }, [options]); const storageTarget = options == null ? void 0 : options.storage; const cookieConfig = options == null ? void 0 : options.cookie; const resolvedStorage = (0, import_react.useMemo)( () => resolveStorage({ storage: storageTarget, cookie: cookieConfig }), [storageTarget, cookieConfig] ); const fallbackRef = (0, import_react.useRef)(UNINITIALIZED); const metadataRef = (0, import_react.useRef)(null); const pendingWriteRef = (0, import_react.useRef)(null); const expiryTimeoutRef = (0, import_react.useRef)(null); const previousKeyRef = (0, import_react.useRef)(key); const clearExpiryTimerRef = (0, import_react.useRef)(() => void 0); const scheduleExpiryRef = (0, import_react.useRef)(() => void 0); const getFallback = (0, import_react.useCallback)(() => { if (fallbackRef.current === UNINITIALIZED) { fallbackRef.current = resolveInitial(initialValue); } return fallbackRef.current; }, [initialValue]); const reportError = (0, import_react.useCallback)( (error, operation) => { var _a, _b; if (process.env.NODE_ENV !== "production") { console.warn(`useSyncedStorage: ${operation} error for key "${key}"`, error); } (_b = (_a = optionsRef.current) == null ? void 0 : _a.onError) == null ? void 0 : _b.call(_a, error, { operation, key }); }, [key] ); const removeItem = (0, import_react.useCallback)(() => { try { resolvedStorage.storage.removeItem(key); } catch (error) { reportError(error, "remove"); } }, [key, reportError, resolvedStorage.storage]); const writeValue = (0, import_react.useCallback)( (value, preservedExpiresAt) => { var _a, _b; const currentOptions = optionsRef.current; const serializer = (_a = currentOptions == null ? void 0 : currentOptions.serializer) != null ? _a : defaultSerializer; let serialized; try { serialized = serializer(value); } catch (error) { reportError(error, "serialize"); return void 0; } let expiresAt = preservedExpiresAt; if ((currentOptions == null ? void 0 : currentOptions.ttl) !== void 0) { expiresAt = Date.now() + currentOptions.ttl; } const meta = {}; if ((currentOptions == null ? void 0 : currentOptions.version) !== void 0) { meta.version = currentOptions.version; } if (expiresAt !== void 0) { meta.expiresAt = expiresAt; } const envelope = { [SYNC_FLAG]: true, data: serialized }; if (meta.version !== void 0 || meta.expiresAt !== void 0) { envelope.meta = meta; } let payload; try { payload = JSON.stringify(envelope); } catch (error) { reportError(error, "serialize"); return void 0; } try { resolvedStorage.storage.setItem(key, payload); } catch (error) { reportError(error, "write"); return void 0; } metadataRef.current = (_b = envelope.meta) != null ? _b : null; scheduleExpiryRef.current(meta.expiresAt); return value; }, [key, reportError, resolvedStorage.storage] ); const interpretAndResolve = (0, import_react.useCallback)(() => { var _a, _b, _c, _d, _e, _f, _g; const fallback = getFallback(); let raw = null; try { raw = resolvedStorage.storage.getItem(key); } catch (error) { reportError(error, "read"); metadataRef.current = null; return fallback; } const result = interpretStoredValue(raw, optionsRef.current, reportError); if (result.kind === "empty") { metadataRef.current = null; return fallback; } if (result.kind === "error") { metadataRef.current = null; return fallback; } if (result.kind === "expired") { metadataRef.current = null; fallbackRef.current = UNINITIALIZED; removeItem(); const expiryEvent = { type: "expiry", key }; (_b = (_a = optionsRef.current) == null ? void 0 : _a.onExpire) == null ? void 0 : _b.call(_a, { key }); (_d = (_c = optionsRef.current) == null ? void 0 : _c.onExternalChange) == null ? void 0 : _d.call(_c, { key, value: void 0, event: expiryEvent }); return getFallback(); } metadataRef.current = (_e = result.meta) != null ? _e : null; const currentOptions = optionsRef.current; let value = result.value; const targetVersion = currentOptions == null ? void 0 : currentOptions.version; const storedVersion = (_f = result.meta) == null ? void 0 : _f.version; if (targetVersion !== void 0 && targetVersion !== storedVersion && (currentOptions == null ? void 0 : currentOptions.migrate)) { try { value = currentOptions.migrate(value, storedVersion); pendingWriteRef.current = { value, expiresAt: (_g = result.meta) == null ? void 0 : _g.expiresAt }; } catch (error) { reportError(error, "migrate"); return fallback; } } return value; }, [getFallback, key, removeItem, reportError, resolvedStorage.storage]); const [storedValue, setStoredValue] = (0, import_react.useState)(interpretAndResolve); const clearExpiryTimer = (0, import_react.useCallback)(() => { if (expiryTimeoutRef.current !== null) { clearTimeout(expiryTimeoutRef.current); expiryTimeoutRef.current = null; } }, []); clearExpiryTimerRef.current = clearExpiryTimer; const handleExpiry = (0, import_react.useCallback)( (source, details) => { var _a, _b, _c, _d, _e; clearExpiryTimer(); metadataRef.current = null; fallbackRef.current = UNINITIALIZED; if (!(details == null ? void 0 : details.alreadyRemoved)) { removeItem(); } if (source !== "external") { (_b = (_a = optionsRef.current) == null ? void 0 : _a.onExpire) == null ? void 0 : _b.call(_a, { key }); } const expiryEvent = { type: "expiry", key }; const event = (_c = details == null ? void 0 : details.event) != null ? _c : expiryEvent; (_e = (_d = optionsRef.current) == null ? void 0 : _d.onExternalChange) == null ? void 0 : _e.call(_d, { key, value: void 0, event }); }, [clearExpiryTimer, key, removeItem] ); const scheduleExpiry = (0, import_react.useCallback)( (expiresAt) => { clearExpiryTimer(); if (expiresAt === void 0) { return; } if (typeof window === "undefined") { return; } const delay = expiresAt - Date.now(); if (delay <= 0) { handleExpiry("timer"); const fallback = getFallback(); setStoredValue(fallback); return; } expiryTimeoutRef.current = window.setTimeout(() => { handleExpiry("timer"); const fallback = getFallback(); setStoredValue(fallback); }, delay); }, [clearExpiryTimer, getFallback, handleExpiry] ); scheduleExpiryRef.current = scheduleExpiry; const setValue = (0, import_react.useCallback)( (value) => { setStoredValue((previous) => { const next = value instanceof Function ? value(previous) : value; if (next === void 0) { clearExpiryTimerRef.current(); metadataRef.current = null; fallbackRef.current = UNINITIALIZED; removeItem(); return getFallback(); } const written = writeValue(next); return written != null ? written : previous; }); }, [getFallback, removeItem, writeValue] ); (0, import_react.useEffect)(() => { if (previousKeyRef.current === key) { return; } previousKeyRef.current = key; fallbackRef.current = UNINITIALIZED; setStoredValue(interpretAndResolve()); }, [interpretAndResolve, key]); (0, import_react.useEffect)(() => { fallbackRef.current = UNINITIALIZED; setStoredValue(interpretAndResolve()); }, [initialValue, interpretAndResolve]); (0, import_react.useEffect)(() => { const pendingWrite = pendingWriteRef.current; if (!pendingWrite) { return; } pendingWriteRef.current = null; writeValue(pendingWrite.value, pendingWrite.expiresAt); }, [writeValue, storedValue]); (0, import_react.useEffect)(() => { var _a; const expiresAt = (_a = metadataRef.current) == null ? void 0 : _a.expiresAt; if (expiresAt === void 0) { clearExpiryTimer(); return; } scheduleExpiry(expiresAt); }, [clearExpiryTimer, scheduleExpiry, storedValue]); (0, import_react.useEffect)(() => { if (!resolvedStorage.listenToStorageEvent || typeof window === "undefined") { return; } const handleStorageChange = (event) => { var _a, _b, _c, _d, _e; if (event.key !== key || event.storageArea !== resolvedStorage.storage) { return; } if (event.newValue === null) { handleExpiry("external", { event, alreadyRemoved: true }); const fallback = getFallback(); setStoredValue(fallback); return; } const result = interpretStoredValue( event.newValue, optionsRef.current, reportError ); if (result.kind === "error") { return; } if (result.kind === "expired") { handleExpiry("external", { event, alreadyRemoved: true }); const fallback = getFallback(); setStoredValue(fallback); return; } metadataRef.current = (_a = result.meta) != null ? _a : null; const currentOptions = optionsRef.current; let value = result.value; const targetVersion = currentOptions == null ? void 0 : currentOptions.version; const storedVersion = (_b = result.meta) == null ? void 0 : _b.version; if (targetVersion !== void 0 && targetVersion !== storedVersion && (currentOptions == null ? void 0 : currentOptions.migrate)) { try { value = currentOptions.migrate(value, storedVersion); pendingWriteRef.current = { value, expiresAt: (_c = result.meta) == null ? void 0 : _c.expiresAt }; } catch (error) { reportError(error, "migrate"); return; } } (_e = (_d = optionsRef.current) == null ? void 0 : _d.onExternalChange) == null ? void 0 : _e.call(_d, { key, value, event }); setStoredValue(value); }; window.addEventListener("storage", handleStorageChange); return () => window.removeEventListener("storage", handleStorageChange); }, [getFallback, handleExpiry, key, reportError, resolvedStorage.storage, resolvedStorage.listenToStorageEvent]); (0, import_react.useEffect)(() => () => clearExpiryTimer(), [clearExpiryTimer]); return [storedValue, setValue]; } function useLocalStorage(key, initialValue, options) { return useSyncedStorage(key, initialValue, { ...options, storage: "local" }); } function useObjectLocalStorage(key, initialValue, options) { const initialRef = (0, import_react.useRef)(initialValue); (0, import_react.useEffect)(() => { initialRef.current = initialValue; }, [initialValue]); const [value, setValue] = useLocalStorage(key, initialValue, options); const setPartial = (0, import_react.useCallback)( (update) => { setValue((previous) => { const base = previous != null ? previous : resolveInitial(initialRef.current); const patch = update instanceof Function ? update(base) : update; return { ...base, ...patch }; }); }, [setValue] ); const reset = (0, import_react.useCallback)(() => { setValue(void 0); }, [setValue]); return [value, setValue, { setPartial, reset }]; } var index_default = useLocalStorage; function useCookieStorage(key, initialValue, options) { return useSyncedStorage(key, initialValue, { ...options, storage: "cookie" }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createCookieStorage, useCookieStorage, useLocalStorage, useObjectLocalStorage, useSyncedStorage }); //# sourceMappingURL=index.cjs.map