@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
JavaScript
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
;