From aa6ad49e6c89e5e3e05ff04e77ac56b36395080e Mon Sep 17 00:00:00 2001 From: Abhishek Chaurasiya Date: Sun, 26 Oct 2025 21:23:06 +0530 Subject: [PATCH] Add useCookieStorage hook and CookieOptions type for cookie management --- index.d.ts | 14 +++++++ index.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/index.d.ts b/index.d.ts index db01dc2..c0620b8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -85,6 +85,14 @@ export type CustomQueue = { queue: T[]; }; +export type CookieOptions = { + /** Number of days from now or a Date object */ + expires?: number | Date; + path?: string; + sameSite?: "Lax" | "Strict" | "None"; + secure?: boolean; +}; + export type RenderInfo = { name: string; renders: number; @@ -181,6 +189,12 @@ declare module "@uidotdev/usehooks" { initialValue?: T ): [T, React.Dispatch>]; + export function useCookieStorage( + key: string, + initialValue?: T, + options?: CookieOptions + ): [T, React.Dispatch>]; + export function useLockBodyScroll(): void; export function useLongPress( diff --git a/index.js b/index.js index f6e4fe2..071018d 100644 --- a/index.js +++ b/index.js @@ -1212,6 +1212,111 @@ export function useSessionStorage(key, initialValue) { return [store ? JSON.parse(store) : initialValue, setState]; } +const setCookieItem = (key, value, options = {}) => { + const { expires = 365, path = '/', sameSite = 'Lax', secure = false } = options; + + const stringifiedValue = JSON.stringify(value); + const expiresDate = + expires instanceof Date + ? expires + : (() => { + const d = new Date(); + d.setDate(d.getDate() + expires); + return d; + })(); + + const cookieParts = [ + `${key}=${encodeURIComponent(stringifiedValue)}`, + `path=${path}`, + `expires=${expiresDate.toUTCString()}`, + `SameSite=${sameSite}`, + ]; + + if (secure) cookieParts.push('secure'); + + document.cookie = cookieParts.join('; '); + dispatchStorageEvent(key, stringifiedValue); +}; + +const removeCookieItem = (key, options = {}) => { + const { path = '/', sameSite = 'Lax', secure = false } = options; + const cookieParts = [ + `${key}=`, + `path=${path}`, + 'expires=Thu, 01 Jan 1970 00:00:00 GMT', + `SameSite=${sameSite}`, + ]; + + if (secure) cookieParts.push('secure'); + + document.cookie = cookieParts.join('; '); + dispatchStorageEvent(key, null); +}; + +const getCookieItem = (key) => { + const cookies = document.cookie ? document.cookie.split('; ') : []; + + for (let i = 0; i < cookies.length; i++) { + const [k, ...v] = cookies[i].split('='); + if (k === key) { + try { + return decodeURIComponent(v.join('=')); + } catch (e) { + return v.join('='); + } + } + } + + return null; +}; + +const useCookieStorageSubscribe = (callback) => { + // cookies do not emit a native event when changed, but we'll listen to + // storage so cross-tab updates triggered by our helpers (which dispatch a + // StorageEvent) will be picked up. + window.addEventListener("storage", callback); + return () => window.removeEventListener("storage", callback); +}; + +const getCookieServerSnapshot = () => { + throw Error("useCookieStorage is a client-only hook"); +}; + +export function useCookieStorage(key, initialValue, options = {}) { + const getSnapshot = () => getCookieItem(key); + + const store = React.useSyncExternalStore( + useCookieStorageSubscribe, + getSnapshot, + getCookieServerSnapshot + ); + + const setState = React.useCallback( + (v) => { + try { + const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; + + if (nextState === undefined || nextState === null) { + removeCookieItem(key, options); + } else { + setCookieItem(key, nextState, options); + } + } catch (e) { + console.warn(e); + } + }, + [key, store, options] + ); + + React.useEffect(() => { + if (getCookieItem(key) === null && typeof initialValue !== "undefined") { + setCookieItem(key, initialValue, options); + } + }, [key, initialValue, options]); + + return [store ? JSON.parse(store) : initialValue, setState]; +} + export function useSet(values) { const setRef = React.useRef(new Set(values)); const [, reRender] = React.useReducer((x) => x + 1, 0);