Created
October 30, 2025 21:04
-
-
Save Avi-E-Koenig/c900d6558768e64feec26c9ad26eb623 to your computer and use it in GitHub Desktop.
NextJS localstorage hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useCallback, useEffect, useRef, useState } from 'react' | |
| import loggerClient from '@/lib/logger-client' | |
| const useLocalStorage = <T>( | |
| key: string, | |
| defaultValue: T, | |
| ): [T, (value: T | ((prev: T) => T)) => void] => { | |
| const [value, setValue] = useState<T>(defaultValue) | |
| const [isHydrated, setIsHydrated] = useState(false) | |
| // Store default value reference | |
| const defaultValueRef = useRef(defaultValue) | |
| // Track if we've already hydrated to prevent re-initialization | |
| const hasHydratedRef = useRef(false) | |
| // Update default value ref when defaultValue changes | |
| useEffect(() => { | |
| defaultValueRef.current = defaultValue | |
| }, [defaultValue]) | |
| // Hydration effect - sync with localStorage after mount (runs only once per key) | |
| useEffect(() => { | |
| if (typeof window === 'undefined' || hasHydratedRef.current) return | |
| try { | |
| const storedValue = window.localStorage.getItem(key) | |
| if (storedValue !== null) { | |
| const parsedValue = JSON.parse(storedValue) | |
| setValue(parsedValue) | |
| } | |
| // Don't set default value during hydration - only when actively updating | |
| } catch (error) { | |
| loggerClient({ | |
| args: { error, key }, | |
| logType: 'error', | |
| description: 'Failed to parse localStorage value during hydration', | |
| }) | |
| } | |
| setIsHydrated(true) | |
| hasHydratedRef.current = true | |
| }, [key]) // Key dependency allows re-hydrating when key changes | |
| const updateValue = useCallback( | |
| (newValue: T | ((prev: T) => T)) => { | |
| if (typeof window === 'undefined' || !isHydrated) return | |
| try { | |
| setValue((prev) => { | |
| const valueToStore = | |
| typeof newValue === 'function' | |
| ? (newValue as (prev: T) => T)(prev) | |
| : newValue | |
| window.localStorage.setItem(key, JSON.stringify(valueToStore)) | |
| return valueToStore | |
| }) | |
| } catch (error) { | |
| loggerClient({ | |
| args: { error, key, newValue }, | |
| logType: 'error', | |
| }) | |
| } | |
| }, | |
| [key, isHydrated], | |
| ) | |
| useEffect(() => { | |
| if (typeof window === 'undefined' || !isHydrated) return | |
| const handleStorageChange = (event: StorageEvent) => { | |
| if (event.key === key) { | |
| try { | |
| const newValue = | |
| JSON.parse(event.newValue!) ?? defaultValueRef.current | |
| setValue(newValue) | |
| } catch (error) { | |
| loggerClient({ | |
| args: { error, key, event }, | |
| logType: 'error', | |
| }) | |
| } | |
| } | |
| } | |
| window.addEventListener('storage', handleStorageChange) | |
| return () => { | |
| window.removeEventListener('storage', handleStorageChange) | |
| } | |
| }, [key, isHydrated]) | |
| return [value, updateValue] | |
| } | |
| export { useLocalStorage } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment