Skip to content

Instantly share code, notes, and snippets.

@Avi-E-Koenig
Created October 30, 2025 21:04
Show Gist options
  • Select an option

  • Save Avi-E-Koenig/c900d6558768e64feec26c9ad26eb623 to your computer and use it in GitHub Desktop.

Select an option

Save Avi-E-Koenig/c900d6558768e64feec26c9ad26eb623 to your computer and use it in GitHub Desktop.
NextJS localstorage hook
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