Skip to content

Instantly share code, notes, and snippets.

@yuijim
Created April 10, 2026 16:40
Show Gist options
  • Select an option

  • Save yuijim/8ea06e9625140c3371dfec7a52583985 to your computer and use it in GitHub Desktop.

Select an option

Save yuijim/8ea06e9625140c3371dfec7a52583985 to your computer and use it in GitHub Desktop.
"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,
);
}
"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