diff --git a/web/hooks/use-persistent-local-state.ts b/web/hooks/use-persistent-local-state.ts index c6e2a0b6..37dcd5e5 100644 --- a/web/hooks/use-persistent-local-state.ts +++ b/web/hooks/use-persistent-local-state.ts @@ -6,26 +6,63 @@ import {isFunction} from 'web/hooks/use-persistent-in-memory-state' import {useStateCheckEquality} from 'web/hooks/use-state-check-equality' import {safeLocalStorage} from 'web/lib/util/local' -export const usePersistentLocalState = (initialValue: T, key: string) => { +type StoredEnvelope = { + value: T + expiresAt: number | null // null = never expires +} + +const wrapValue = (value: T, ttlMs: number | null): StoredEnvelope => ({ + value, + expiresAt: ttlMs != null ? Date.now() + ttlMs : null, +}) + +const unwrapValue = (envelope: unknown, fallback: T): {value: T; expired: boolean} => { + if (envelope == null || typeof envelope !== 'object' || !('value' in envelope)) { + return {value: fallback, expired: false} + } + + const {value, expiresAt} = envelope as StoredEnvelope + + if (expiresAt != null && Date.now() > expiresAt) { + return {value: fallback, expired: true} + } + + return {value, expired: false} +} + +export const usePersistentLocalState = ( + initialValue: T, + key: string, + ttl: number | null = null, +) => { // Note: use a version (like "-v1") in the key to increment after backwards-incompatible changes const isClient = useIsClient() - const [state, setState] = useStateCheckEquality( - (isClient && safeJsonParse(safeLocalStorage?.getItem(key))) || initialValue, - ) + + const readFromStorage = (): T => { + const raw = safeLocalStorage?.getItem(key) + const parsed = safeJsonParse(raw) + const {value, expired} = unwrapValue(parsed, initialValue) + if (expired) safeLocalStorage?.removeItem(key) + return value + } + + const [state, setState] = useStateCheckEquality(isClient ? readFromStorage() : initialValue) + const saveState = useEvent((newState: T | ((prevState: T) => T)) => { setState((prevState: T) => { const updatedState = isFunction(newState) ? newState(prevState) : newState - safeLocalStorage?.setItem(key, JSON.stringify(updatedState)) + safeLocalStorage?.setItem(key, JSON.stringify(wrapValue(updatedState, ttl))) return updatedState }) }) useEffect(() => { if (safeLocalStorage) { - const storedJson = safeJsonParse(safeLocalStorage.getItem(key)) - const storedValue = storedJson ?? initialValue - if (storedJson === null && initialValue === undefined) return - saveState(storedValue as T) + const raw = safeLocalStorage.getItem(key) + const parsed = safeJsonParse(raw) + const {value, expired} = unwrapValue(parsed, initialValue) + if (expired || (parsed === null && initialValue === undefined)) return + saveState(value as T) } }, [key])