{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-local-storage", "description": "A custom hook to manage local storage state.", "files": [ { "path": "src/registry/new-york/hooks/use-local-storage.ts", "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * A custom hook that manages state synchronized with localStorage.\n * Enhanced with custom serializers, remove functionality, and improved cross-tab sync.\n *\n * @param {string} key The localStorage key to use\n * @param {*} initialValue The initial value if no value exists in localStorage\n * @param {Object} options Configuration options for the hook\n * @param {Object} options.serializer Custom serializer function with read/write methods\n * @param {boolean} options.syncData Whether to sync data across tabs (default: true)\n * @param {boolean} options.initializeWithValue Whether to initialize with value from localStorage (default: true)\n * @returns {[*, function, function]} Returns [value, setValue, removeValue]\n */\n\n/**\n * Options for the useLocalStorage hook\n */\nexport interface UseLocalStorageOptions {\n /** Custom serializer function */\n serializer?: {\n read: (value: string) => T;\n write: (value: T) => string;\n };\n /** Function to determine if the value should be synced across tabs */\n syncData?: boolean;\n /** Custom event name for cross-tab synchronization */\n initializeWithValue?: boolean;\n}\n\n/**\n * Return type for useLocalStorage hook\n */\nexport type UseLocalStorageReturn = [\n T,\n (value: T | ((val: T) => T)) => void,\n () => void,\n];\n\n// Custom hook to create a stable event callback\nfunction useEventCallback(\n fn: (...args: Args) => Return,\n): (...args: Args) => Return {\n const ref = useRef(fn);\n\n useEffect(() => {\n ref.current = fn;\n });\n\n return useCallback((...args: Args) => {\n return ref.current?.(...args);\n }, []);\n}\n\n// Custom hook to listen for localStorage events\nfunction useLocalStorageEventListener(callback: (e: StorageEvent) => void) {\n const eventCallback = useEventCallback(callback);\n\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n\n window.addEventListener(\"storage\", eventCallback);\n return () => window.removeEventListener(\"storage\", eventCallback);\n }, [eventCallback]);\n}\n\n/**\n * Default serializer for localStorage values\n */\nconst defaultSerializer = {\n read: (value: string) => {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n },\n write: (value: unknown) => JSON.stringify(value),\n};\n\n/**\n * Read a value from localStorage\n */\nfunction readLocalStorageValue(\n key: string,\n initialValue: T,\n serializer: { read: (value: string) => T; write: (value: T) => string },\n): T {\n if (typeof window === \"undefined\") {\n return initialValue;\n }\n\n try {\n const item = window.localStorage.getItem(key);\n return item !== null ? serializer.read(item) : initialValue;\n } catch (error) {\n console.warn(`Error reading localStorage key \"${key}\":`, error);\n return initialValue;\n }\n}\n\n/**\n * Write a value to localStorage\n */\nfunction writeLocalStorageValue(\n key: string,\n value: T,\n serializer: { read: (value: string) => T; write: (value: T) => string },\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n try {\n window.localStorage.setItem(key, serializer.write(value));\n } catch (error) {\n console.warn(`Error setting localStorage key \"${key}\":`, error);\n }\n}\n\nexport function useLocalStorage(\n key: string,\n initialValue: T,\n options: UseLocalStorageOptions = {},\n): UseLocalStorageReturn {\n const {\n serializer = defaultSerializer as {\n read: (value: string) => T;\n write: (value: T) => string;\n },\n syncData = true,\n initializeWithValue = true,\n } = options;\n\n // Get the initial value from localStorage or use the provided initial value\n const [storedValue, setStoredValue] = useState(() => {\n if (!initializeWithValue) {\n return initialValue;\n }\n return readLocalStorageValue(key, initialValue, serializer);\n });\n\n // Return a wrapped version of useState's setter function that persists the new value to localStorage\n const setValue = useEventCallback((value: T | ((val: T) => T)) => {\n if (typeof window === \"undefined\") {\n console.warn(\n `Tried setting localStorage key \"${key}\" even though environment is not a client`,\n );\n return;\n }\n\n try {\n // Allow value to be a function so we have the same API as useState\n const newValue = value instanceof Function ? value(storedValue) : value;\n\n // Save to localStorage\n writeLocalStorageValue(key, newValue, serializer);\n\n // Save state\n setStoredValue(newValue);\n\n // Dispatch a custom event to notify other useLocalStorage hooks\n if (syncData) {\n window.dispatchEvent(\n new CustomEvent(\"local-storage\", {\n detail: {\n key,\n newValue,\n },\n }),\n );\n }\n } catch (error) {\n console.warn(`Error setting localStorage key \"${key}\":`, error);\n }\n });\n\n // Function to remove the value from localStorage\n const removeValue = useEventCallback(() => {\n if (typeof window === \"undefined\") {\n console.warn(\n `Tried removing localStorage key \"${key}\" even though environment is not a client`,\n );\n return;\n }\n\n try {\n window.localStorage.removeItem(key);\n setStoredValue(initialValue);\n\n // Dispatch a custom event to notify other useLocalStorage hooks\n if (syncData) {\n window.dispatchEvent(\n new CustomEvent(\"local-storage\", {\n detail: {\n key,\n newValue: initialValue,\n },\n }),\n );\n }\n } catch (error) {\n console.warn(`Error removing localStorage key \"${key}\":`, error);\n }\n });\n\n // Listen for changes to localStorage from other tabs/windows\n useLocalStorageEventListener(\n useEventCallback((e: StorageEvent) => {\n if (e.key === key && e.newValue !== e.oldValue && syncData) {\n try {\n if (e.newValue === null) {\n setStoredValue(initialValue);\n } else {\n setStoredValue(serializer.read(e.newValue));\n }\n } catch (error) {\n console.warn(`Error parsing localStorage key \"${key}\":`, error);\n }\n }\n }),\n );\n\n // Listen for custom local-storage events (for same-tab synchronization)\n useEffect(() => {\n if (!syncData) return;\n\n const handleCustomEvent = (e: CustomEvent) => {\n if (e.detail?.key === key && e.detail?.newValue !== undefined) {\n setStoredValue(e.detail.newValue);\n }\n };\n\n window.addEventListener(\n \"local-storage\",\n handleCustomEvent as EventListener,\n );\n return () =>\n window.removeEventListener(\n \"local-storage\",\n handleCustomEvent as EventListener,\n );\n }, [key, syncData]);\n\n return [storedValue, setValue, removeValue];\n}\n", "type": "registry:hook" } ], "type": "registry:hook" }