Created
April 10, 2026 16:40
-
-
Save yuijim/8ea06e9625140c3371dfec7a52583985 to your computer and use it in GitHub Desktop.
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
| "use client"; | |
| import { useCallback, useRef, useSyncExternalStore } from "react"; | |
| export function useStorage<T>( | |
| storage: Storage, | |
| key: string, | |
| initialValue: T | null = null, | |
| ) { | |
| const snapshotCache = useRef<{ | |
| serialized: string | null | undefined; | |
| value: T | null; | |
| }>({ serialized: undefined, value: initialValue }); | |
| const getSnapshot = useCallback(() => { | |
| if (typeof window === "undefined") { | |
| return initialValue; | |
| } | |
| const serialized = storage.getItem(key); | |
| if (serialized === snapshotCache.current.serialized) { | |
| return snapshotCache.current.value; | |
| } | |
| snapshotCache.current.serialized = serialized; | |
| snapshotCache.current.value = | |
| serialized === null | |
| ? initialValue | |
| : (() => { | |
| try { | |
| return JSON.parse(serialized) as T; | |
| } catch { | |
| return initialValue; | |
| } | |
| })(); | |
| return snapshotCache.current.value; | |
| }, [initialValue, storage, key]); | |
| const subscribe = useCallback( | |
| (callback: () => void) => { | |
| if (typeof window === "undefined") { | |
| return () => {}; // Return a noop unsubscribe function | |
| } | |
| const handleStorageChange = (event: StorageEvent) => { | |
| if (event.key === key) { | |
| callback(); | |
| } | |
| }; | |
| window.addEventListener("storage", handleStorageChange); | |
| return () => window.removeEventListener("storage", handleStorageChange); | |
| }, | |
| [key], | |
| ); | |
| const snapshot = useSyncExternalStore( | |
| subscribe, | |
| getSnapshot, | |
| () => initialValue, // Server snapshot | |
| ); | |
| const setValue = useCallback( | |
| (value: T | ((prev: T | null) => T)) => { | |
| // Check if we're in the browser | |
| if (typeof window === "undefined") { | |
| return; | |
| } | |
| const nextValue = | |
| typeof value === "function" | |
| ? (value as (prev: T | null) => T)(getSnapshot()) | |
| : value; | |
| storage.setItem(key, JSON.stringify(nextValue)); | |
| const event = new StorageEvent("storage", { | |
| key, | |
| newValue: JSON.stringify(nextValue), | |
| storageArea: storage, | |
| url: window.location.href, | |
| }); | |
| window.dispatchEvent(event); | |
| }, | |
| [storage, key, getSnapshot], | |
| ); | |
| return [snapshot, setValue] as const; | |
| } | |
| export function useLocalStorage<T>(key: string, initialValue: T | null = null) { | |
| return useStorage( | |
| typeof window !== "undefined" ? localStorage : ({} as Storage), | |
| key, | |
| initialValue, | |
| ); | |
| } | |
| export function useSessionStorage<T>( | |
| key: string, | |
| initialValue: T | null = null, | |
| ) { | |
| return useStorage( | |
| typeof window !== "undefined" ? sessionStorage : ({} as Storage), | |
| key, | |
| initialValue, | |
| ); | |
| } |
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
| "use client"; | |
| import { | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useState, | |
| useSyncExternalStore | |
| } from "react"; | |
| import { useLocalStorage } from "./useStorage"; | |
| export const UPDATE_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes | |
| export function useUserPosition( | |
| onChange?: (position: GeolocationPosition | null) => void, | |
| onError?: (error: GeolocationPositionError | null) => void, | |
| ) { | |
| const [hadUserInteraction, setHadUserInteraction] = useState(false); | |
| const [geolocationActive, setGeolocationActive] = useLocalStorage< | |
| boolean | null | |
| >( | |
| "geolocationActive", | |
| null, // null means that the user has not made a choice yet | |
| ); | |
| const [position, setPosition] = useState<GeolocationPosition | null>(null); | |
| const [error, setError] = useState<GeolocationPositionError | null>(null); | |
| const geolocationAvailable = useSyncExternalStore( | |
| () => { | |
| return () => {}; | |
| }, | |
| () => "geolocation" in navigator, | |
| () => false, | |
| ); | |
| const enabled = useMemo( | |
| () => | |
| geolocationAvailable && geolocationActive === true && hadUserInteraction, | |
| [geolocationAvailable, geolocationActive, hadUserInteraction], | |
| ); | |
| const positionSubscriber = useCallback( | |
| (callback: () => void) => { | |
| if (!enabled) return () => {}; | |
| const watchId = navigator.geolocation.watchPosition( | |
| (position) => { | |
| setPosition(position); | |
| setError(null); | |
| if (onChange) onChange(position); | |
| callback(); | |
| }, | |
| (error) => { | |
| setPosition(null); | |
| setError(error); | |
| if (onError) onError(error); | |
| callback(); | |
| }, | |
| { | |
| enableHighAccuracy: false, | |
| maximumAge: UPDATE_INTERVAL_MS, | |
| timeout: 30 * 1000, // 30 seconds | |
| }, | |
| ); | |
| return () => { | |
| navigator.geolocation.clearWatch(watchId); | |
| }; | |
| }, | |
| [enabled, onChange, onError], | |
| ); | |
| const positionGetSnapshot = useCallback(() => { | |
| if (!enabled) return null; | |
| return position; | |
| }, [enabled, position]); | |
| const positionStore = useSyncExternalStore( | |
| positionSubscriber, | |
| positionGetSnapshot, | |
| () => null, | |
| ); | |
| const handleUserInteraction = useCallback(() => { | |
| setHadUserInteraction(true); | |
| }, []); | |
| useEffect(() => { | |
| document.addEventListener("mousedown", handleUserInteraction, { | |
| once: true, | |
| }); | |
| document.addEventListener("keydown", handleUserInteraction, { once: true }); | |
| document.addEventListener("touchstart", handleUserInteraction, { | |
| once: true, | |
| }); | |
| return () => { | |
| document.removeEventListener("mousedown", handleUserInteraction); | |
| document.removeEventListener("keydown", handleUserInteraction); | |
| document.removeEventListener("touchstart", handleUserInteraction); | |
| }; | |
| }, [handleUserInteraction]); | |
| return { | |
| geolocationAvailable, | |
| geolocationActive, | |
| setGeolocationActive, | |
| position: positionStore, | |
| error, | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment