Created
May 12, 2024 20:38
-
-
Save oleg1994/3abdbf9f5277d4dc472ad4f8e8110286 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
| import * as React from "react"; | |
| function isShallowEqual(object1, object2) { | |
| const keys1 = Object.keys(object1); | |
| const keys2 = Object.keys(object2); | |
| if (keys1.length !== keys2.length) { | |
| return false; | |
| } | |
| for (let key of keys1) { | |
| if (object1[key] !== object2[key]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function isTouchEvent({ nativeEvent }) { | |
| return window.TouchEvent | |
| ? nativeEvent instanceof TouchEvent | |
| : "touches" in nativeEvent; | |
| } | |
| function isMouseEvent(event) { | |
| return event.nativeEvent instanceof MouseEvent; | |
| } | |
| function throttle(cb, ms) { | |
| let lastTime = 0; | |
| return () => { | |
| const now = Date.now(); | |
| if (now - lastTime >= ms) { | |
| cb(); | |
| lastTime = now; | |
| } | |
| }; | |
| } | |
| function isPlainObject(value) { | |
| return Object.prototype.toString.call(value) === "[object Object]"; | |
| } | |
| function dispatchStorageEvent(key, newValue) { | |
| window.dispatchEvent(new StorageEvent("storage", { key, newValue })); | |
| } | |
| export function useBattery() { | |
| const [state, setState] = React.useState({ | |
| supported: true, | |
| loading: true, | |
| level: null, | |
| charging: null, | |
| chargingTime: null, | |
| dischargingTime: null, | |
| }); | |
| React.useEffect(() => { | |
| if (!navigator.getBattery) { | |
| setState((s) => ({ | |
| ...s, | |
| supported: false, | |
| loading: false, | |
| })); | |
| return; | |
| } | |
| let battery = null; | |
| const handleChange = () => { | |
| setState({ | |
| supported: true, | |
| loading: false, | |
| level: battery.level, | |
| charging: battery.charging, | |
| chargingTime: battery.chargingTime, | |
| dischargingTime: battery.dischargingTime, | |
| }); | |
| }; | |
| navigator.getBattery().then((b) => { | |
| battery = b; | |
| handleChange(); | |
| b.addEventListener("levelchange", handleChange); | |
| b.addEventListener("chargingchange", handleChange); | |
| b.addEventListener("chargingtimechange", handleChange); | |
| b.addEventListener("dischargingtimechange", handleChange); | |
| }); | |
| return () => { | |
| if (battery) { | |
| battery.removeEventListener("levelchange", handleChange); | |
| battery.removeEventListener("chargingchange", handleChange); | |
| battery.removeEventListener("chargingtimechange", handleChange); | |
| battery.removeEventListener("dischargingtimechange", handleChange); | |
| } | |
| }; | |
| }, []); | |
| return state; | |
| } | |
| export function useClickAway(cb) { | |
| const ref = React.useRef(null); | |
| const refCb = React.useRef(cb); | |
| React.useLayoutEffect(() => { | |
| refCb.current = cb; | |
| }); | |
| React.useEffect(() => { | |
| const handler = (e) => { | |
| const element = ref.current; | |
| if (element && !element.contains(e.target)) { | |
| refCb.current(e); | |
| } | |
| }; | |
| document.addEventListener("mousedown", handler); | |
| document.addEventListener("touchstart", handler); | |
| return () => { | |
| document.removeEventListener("mousedown", handler); | |
| document.removeEventListener("touchstart", handler); | |
| }; | |
| }, []); | |
| return ref; | |
| } | |
| function oldSchoolCopy(text) { | |
| const tempTextArea = document.createElement("textarea"); | |
| tempTextArea.value = text; | |
| document.body.appendChild(tempTextArea); | |
| tempTextArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(tempTextArea); | |
| } | |
| export function useCopyToClipboard() { | |
| const [state, setState] = React.useState(null); | |
| const copyToClipboard = React.useCallback((value) => { | |
| const handleCopy = async () => { | |
| try { | |
| if (navigator?.clipboard?.writeText) { | |
| await navigator.clipboard.writeText(value); | |
| setState(value); | |
| } else { | |
| throw new Error("writeText not supported"); | |
| } | |
| } catch (e) { | |
| oldSchoolCopy(value); | |
| setState(value); | |
| } | |
| }; | |
| handleCopy(); | |
| }, []); | |
| return [state, copyToClipboard]; | |
| } | |
| export function useCounter(startingValue = 0, options = {}) { | |
| const { min, max } = options; | |
| if (typeof min === "number" && startingValue < min) { | |
| throw new Error( | |
| `Your starting value of ${startingValue} is less than your min of ${min}.` | |
| ); | |
| } | |
| if (typeof max === "number" && startingValue > max) { | |
| throw new Error( | |
| `Your starting value of ${startingValue} is greater than your max of ${max}.` | |
| ); | |
| } | |
| const [count, setCount] = React.useState(startingValue); | |
| const increment = React.useCallback(() => { | |
| setCount((c) => { | |
| const nextCount = c + 1; | |
| if (typeof max === "number" && nextCount > max) { | |
| return c; | |
| } | |
| return nextCount; | |
| }); | |
| }, [max]); | |
| const decrement = React.useCallback(() => { | |
| setCount((c) => { | |
| const nextCount = c - 1; | |
| if (typeof min === "number" && nextCount < min) { | |
| return c; | |
| } | |
| return nextCount; | |
| }); | |
| }, [min]); | |
| const set = React.useCallback( | |
| (nextCount) => { | |
| setCount((c) => { | |
| if (typeof max === "number" && nextCount > max) { | |
| return c; | |
| } | |
| if (typeof min === "number" && nextCount < min) { | |
| return c; | |
| } | |
| return nextCount; | |
| }); | |
| }, | |
| [max, min] | |
| ); | |
| const reset = React.useCallback(() => { | |
| setCount(startingValue); | |
| }, [startingValue]); | |
| return [ | |
| count, | |
| { | |
| increment, | |
| decrement, | |
| set, | |
| reset, | |
| }, | |
| ]; | |
| } | |
| export function useDebounce(value, delay) { | |
| const [debouncedValue, setDebouncedValue] = React.useState(value); | |
| React.useEffect(() => { | |
| const handler = setTimeout(() => { | |
| setDebouncedValue(value); | |
| }, delay); | |
| return () => { | |
| clearTimeout(handler); | |
| }; | |
| }, [value, delay]); | |
| return debouncedValue; | |
| } | |
| export function useDefault(initialValue, defaultValue) { | |
| const [state, setState] = React.useState(initialValue); | |
| if (typeof state === "undefined" || state === null) { | |
| return [defaultValue, setState]; | |
| } | |
| return [state, setState]; | |
| } | |
| export function useDocumentTitle(title) { | |
| React.useEffect(() => { | |
| document.title = title; | |
| }, [title]); | |
| } | |
| export function useFavicon(url) { | |
| React.useEffect(() => { | |
| let link = document.querySelector(`link[rel~="icon"]`); | |
| if (!link) { | |
| link = document.createElement("link"); | |
| link.type = "image/x-icon"; | |
| link.rel = "icon"; | |
| link.href = url; | |
| document.head.appendChild(link); | |
| } else { | |
| link.href = url; | |
| } | |
| }, [url]); | |
| } | |
| export function useGeolocation(options = {}) { | |
| const [state, setState] = React.useState({ | |
| loading: true, | |
| accuracy: null, | |
| altitude: null, | |
| altitudeAccuracy: null, | |
| heading: null, | |
| latitude: null, | |
| longitude: null, | |
| speed: null, | |
| timestamp: null, | |
| error: null, | |
| }); | |
| const optionsRef = React.useRef(options); | |
| React.useEffect(() => { | |
| const onEvent = ({ coords, timestamp }) => { | |
| setState({ | |
| loading: false, | |
| timestamp, | |
| latitude: coords.latitude, | |
| longitude: coords.longitude, | |
| altitude: coords.altitude, | |
| accuracy: coords.accuracy, | |
| altitudeAccuracy: coords.altitudeAccuracy, | |
| heading: coords.heading, | |
| speed: coords.speed, | |
| }); | |
| }; | |
| const onEventError = (error) => { | |
| setState((s) => ({ | |
| ...s, | |
| loading: false, | |
| error, | |
| })); | |
| }; | |
| navigator.geolocation.getCurrentPosition( | |
| onEvent, | |
| onEventError, | |
| optionsRef.current | |
| ); | |
| const watchId = navigator.geolocation.watchPosition( | |
| onEvent, | |
| onEventError, | |
| optionsRef.current | |
| ); | |
| return () => { | |
| navigator.geolocation.clearWatch(watchId); | |
| }; | |
| }, []); | |
| return state; | |
| } | |
| const initialUseHistoryStateState = { | |
| past: [], | |
| present: null, | |
| future: [], | |
| }; | |
| const useHistoryStateReducer = (state, action) => { | |
| const { past, present, future } = state; | |
| if (action.type === "UNDO") { | |
| return { | |
| past: past.slice(0, past.length - 1), | |
| present: past[past.length - 1], | |
| future: [present, ...future], | |
| }; | |
| } else if (action.type === "REDO") { | |
| return { | |
| past: [...past, present], | |
| present: future[0], | |
| future: future.slice(1), | |
| }; | |
| } else if (action.type === "SET") { | |
| const { newPresent } = action; | |
| if (action.newPresent === present) { | |
| return state; | |
| } | |
| return { | |
| past: [...past, present], | |
| present: newPresent, | |
| future: [], | |
| }; | |
| } else if (action.type === "CLEAR") { | |
| return { | |
| ...initialUseHistoryStateState, | |
| present: action.initialPresent, | |
| }; | |
| } else { | |
| throw new Error("Unsupported action type"); | |
| } | |
| }; | |
| export function useHistoryState(initialPresent = {}) { | |
| const initialPresentRef = React.useRef(initialPresent); | |
| const [state, dispatch] = React.useReducer(useHistoryStateReducer, { | |
| ...initialUseHistoryStateState, | |
| present: initialPresentRef.current, | |
| }); | |
| const canUndo = state.past.length !== 0; | |
| const canRedo = state.future.length !== 0; | |
| const undo = React.useCallback(() => { | |
| if (canUndo) { | |
| dispatch({ type: "UNDO" }); | |
| } | |
| }, [canUndo]); | |
| const redo = React.useCallback(() => { | |
| if (canRedo) { | |
| dispatch({ type: "REDO" }); | |
| } | |
| }, [canRedo]); | |
| const set = React.useCallback( | |
| (newPresent) => dispatch({ type: "SET", newPresent }), | |
| [] | |
| ); | |
| const clear = React.useCallback( | |
| () => | |
| dispatch({ type: "CLEAR", initialPresent: initialPresentRef.current }), | |
| [] | |
| ); | |
| return { state: state.present, set, undo, redo, clear, canUndo, canRedo }; | |
| } | |
| export function useHover() { | |
| const [hovering, setHovering] = React.useState(false); | |
| const previousNode = React.useRef(null); | |
| const handleMouseEnter = React.useCallback(() => { | |
| setHovering(true); | |
| }, []); | |
| const handleMouseLeave = React.useCallback(() => { | |
| setHovering(false); | |
| }, []); | |
| const customRef = React.useCallback( | |
| (node) => { | |
| if (previousNode.current?.nodeType === Node.ELEMENT_NODE) { | |
| previousNode.current.removeEventListener( | |
| "mouseenter", | |
| handleMouseEnter | |
| ); | |
| previousNode.current.removeEventListener( | |
| "mouseleave", | |
| handleMouseLeave | |
| ); | |
| } | |
| if (node?.nodeType === Node.ELEMENT_NODE) { | |
| node.addEventListener("mouseenter", handleMouseEnter); | |
| node.addEventListener("mouseleave", handleMouseLeave); | |
| } | |
| previousNode.current = node; | |
| }, | |
| [handleMouseEnter, handleMouseLeave] | |
| ); | |
| return [customRef, hovering]; | |
| } | |
| export function useIdle(ms = 1000 * 60) { | |
| const [idle, setIdle] = React.useState(false); | |
| React.useEffect(() => { | |
| let timeoutId; | |
| const handleTimeout = () => { | |
| setIdle(true); | |
| }; | |
| const handleEvent = throttle((e) => { | |
| setIdle(false); | |
| window.clearTimeout(timeoutId); | |
| timeoutId = window.setTimeout(handleTimeout, ms); | |
| }, 500); | |
| const handleVisibilityChange = () => { | |
| if (!document.hidden) { | |
| handleEvent(); | |
| } | |
| }; | |
| timeoutId = window.setTimeout(handleTimeout, ms); | |
| window.addEventListener("mousemove", handleEvent); | |
| window.addEventListener("mousedown", handleEvent); | |
| window.addEventListener("resize", handleEvent); | |
| window.addEventListener("keydown", handleEvent); | |
| window.addEventListener("touchstart", handleEvent); | |
| window.addEventListener("wheel", handleEvent); | |
| document.addEventListener("visibilitychange", handleVisibilityChange); | |
| return () => { | |
| window.removeEventListener("mousemove", handleEvent); | |
| window.removeEventListener("mousedown", handleEvent); | |
| window.removeEventListener("resize", handleEvent); | |
| window.removeEventListener("keydown", handleEvent); | |
| window.removeEventListener("touchstart", handleEvent); | |
| window.removeEventListener("wheel", handleEvent); | |
| document.removeEventListener("visibilitychange", handleVisibilityChange); | |
| window.clearTimeout(timeoutId); | |
| }; | |
| }, [ms]); | |
| return idle; | |
| } | |
| export function useIntersectionObserver(options = {}) { | |
| const { threshold = 1, root = null, rootMargin = "0px" } = options; | |
| const [entry, setEntry] = React.useState(null); | |
| const previousObserver = React.useRef(null); | |
| const customRef = React.useCallback( | |
| (node) => { | |
| if (previousObserver.current) { | |
| previousObserver.current.disconnect(); | |
| previousObserver.current = null; | |
| } | |
| if (node?.nodeType === Node.ELEMENT_NODE) { | |
| const observer = new IntersectionObserver( | |
| ([entry]) => { | |
| setEntry(entry); | |
| }, | |
| { threshold, root, rootMargin } | |
| ); | |
| observer.observe(node); | |
| previousObserver.current = observer; | |
| } | |
| }, | |
| [threshold, root, rootMargin] | |
| ); | |
| return [customRef, entry]; | |
| } | |
| export function useIsClient() { | |
| const [isClient, setIsClient] = React.useState(false); | |
| React.useEffect(() => { | |
| setIsClient(true); | |
| }, []); | |
| return isClient; | |
| } | |
| export function useIsFirstRender() { | |
| const renderRef = React.useRef(true); | |
| if (renderRef.current === true) { | |
| renderRef.current = false; | |
| return true; | |
| } | |
| return renderRef.current; | |
| } | |
| export function useList(defaultList = []) { | |
| const [list, setList] = React.useState(defaultList); | |
| const set = React.useCallback((l) => { | |
| setList(l); | |
| }, []); | |
| const push = React.useCallback((element) => { | |
| setList((l) => [...l, element]); | |
| }, []); | |
| const removeAt = React.useCallback((index) => { | |
| setList((l) => [...l.slice(0, index), ...l.slice(index + 1)]); | |
| }, []); | |
| const insertAt = React.useCallback((index, element) => { | |
| setList((l) => [...l.slice(0, index), element, ...l.slice(index)]); | |
| }, []); | |
| const updateAt = React.useCallback((index, element) => { | |
| setList((l) => l.map((e, i) => (i === index ? element : e))); | |
| }, []); | |
| const clear = React.useCallback(() => setList([]), []); | |
| return [list, { set, push, removeAt, insertAt, updateAt, clear }]; | |
| } | |
| const setLocalStorageItem = (key, value) => { | |
| const stringifiedValue = JSON.stringify(value); | |
| window.localStorage.setItem(key, stringifiedValue); | |
| dispatchStorageEvent(key, stringifiedValue); | |
| }; | |
| const removeLocalStorageItem = (key) => { | |
| window.localStorage.removeItem(key); | |
| dispatchStorageEvent(key, null); | |
| }; | |
| const getLocalStorageItem = (key) => { | |
| return window.localStorage.getItem(key); | |
| }; | |
| const useLocalStorageSubscribe = (callback) => { | |
| window.addEventListener("storage", callback); | |
| return () => window.removeEventListener("storage", callback); | |
| }; | |
| const getLocalStorageServerSnapshot = () => { | |
| throw Error("useLocalStorage is a client-only hook"); | |
| }; | |
| export function useLocalStorage(key, initialValue) { | |
| const getSnapshot = () => getLocalStorageItem(key); | |
| const store = React.useSyncExternalStore( | |
| useLocalStorageSubscribe, | |
| getSnapshot, | |
| getLocalStorageServerSnapshot | |
| ); | |
| const setState = React.useCallback( | |
| (v) => { | |
| try { | |
| const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; | |
| if (nextState === undefined || nextState === null) { | |
| removeLocalStorageItem(key); | |
| } else { | |
| setLocalStorageItem(key, nextState); | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| } | |
| }, | |
| [key, store] | |
| ); | |
| React.useEffect(() => { | |
| if ( | |
| getLocalStorageItem(key) === null && | |
| typeof initialValue !== "undefined" | |
| ) { | |
| setLocalStorageItem(key, initialValue); | |
| } | |
| }, [key, initialValue]); | |
| return [store ? JSON.parse(store) : initialValue, setState]; | |
| } | |
| export function useLockBodyScroll() { | |
| React.useLayoutEffect(() => { | |
| const originalStyle = window.getComputedStyle(document.body).overflow; | |
| document.body.style.overflow = "hidden"; | |
| return () => { | |
| document.body.style.overflow = originalStyle; | |
| }; | |
| }, []); | |
| } | |
| export function useLongPress(callback, options = {}) { | |
| const { threshold = 400, onStart, onFinish, onCancel } = options; | |
| const isLongPressActive = React.useRef(false); | |
| const isPressed = React.useRef(false); | |
| const timerId = React.useRef(); | |
| return React.useMemo(() => { | |
| if (typeof callback !== "function") { | |
| return {}; | |
| } | |
| const start = (event) => { | |
| if (!isMouseEvent(event) && !isTouchEvent(event)) return; | |
| if (onStart) { | |
| onStart(event); | |
| } | |
| isPressed.current = true; | |
| timerId.current = setTimeout(() => { | |
| callback(event); | |
| isLongPressActive.current = true; | |
| }, threshold); | |
| }; | |
| const cancel = (event) => { | |
| if (!isMouseEvent(event) && !isTouchEvent(event)) return; | |
| if (isLongPressActive.current) { | |
| if (onFinish) { | |
| onFinish(event); | |
| } | |
| } else if (isPressed.current) { | |
| if (onCancel) { | |
| onCancel(event); | |
| } | |
| } | |
| isLongPressActive.current = false; | |
| isPressed.current = false; | |
| if (timerId.current) { | |
| window.clearTimeout(timerId.current); | |
| } | |
| }; | |
| const mouseHandlers = { | |
| onMouseDown: start, | |
| onMouseUp: cancel, | |
| onMouseLeave: cancel, | |
| }; | |
| const touchHandlers = { | |
| onTouchStart: start, | |
| onTouchEnd: cancel, | |
| }; | |
| return { | |
| ...mouseHandlers, | |
| ...touchHandlers, | |
| }; | |
| }, [callback, threshold, onCancel, onFinish, onStart]); | |
| } | |
| export function useMap(initialState) { | |
| const mapRef = React.useRef(new Map(initialState)); | |
| const [, reRender] = React.useReducer((x) => x + 1, 0); | |
| mapRef.current.set = (...args) => { | |
| Map.prototype.set.apply(mapRef.current, args); | |
| reRender(); | |
| return mapRef.current; | |
| }; | |
| mapRef.current.clear = (...args) => { | |
| Map.prototype.clear.apply(mapRef.current, args); | |
| reRender(); | |
| }; | |
| mapRef.current.delete = (...args) => { | |
| const res = Map.prototype.delete.apply(mapRef.current, args); | |
| reRender(); | |
| return res; | |
| }; | |
| return mapRef.current; | |
| } | |
| export function useMeasure() { | |
| const [dimensions, setDimensions] = React.useState({ | |
| width: null, | |
| height: null, | |
| }); | |
| const previousObserver = React.useRef(null); | |
| const customRef = React.useCallback((node) => { | |
| if (previousObserver.current) { | |
| previousObserver.current.disconnect(); | |
| previousObserver.current = null; | |
| } | |
| if (node?.nodeType === Node.ELEMENT_NODE) { | |
| const observer = new ResizeObserver(([entry]) => { | |
| if (entry && entry.borderBoxSize) { | |
| const { inlineSize: width, blockSize: height } = | |
| entry.borderBoxSize[0]; | |
| setDimensions({ width, height }); | |
| } | |
| }); | |
| observer.observe(node); | |
| previousObserver.current = observer; | |
| } | |
| }, []); | |
| return [customRef, dimensions]; | |
| } | |
| export function useMediaQuery(query) { | |
| const subscribe = React.useCallback( | |
| (callback) => { | |
| const matchMedia = window.matchMedia(query); | |
| matchMedia.addEventListener("change", callback); | |
| return () => { | |
| matchMedia.removeEventListener("change", callback); | |
| }; | |
| }, | |
| [query] | |
| ); | |
| const getSnapshot = () => { | |
| return window.matchMedia(query).matches; | |
| }; | |
| const getServerSnapshot = () => { | |
| throw Error("useMediaQuery is a client-only hook"); | |
| }; | |
| return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); | |
| } | |
| export function useMouse() { | |
| const [state, setState] = React.useState({ | |
| x: 0, | |
| y: 0, | |
| elementX: 0, | |
| elementY: 0, | |
| elementPositionX: 0, | |
| elementPositionY: 0, | |
| }); | |
| const ref = React.useRef(null); | |
| React.useLayoutEffect(() => { | |
| const handleMouseMove = (event) => { | |
| let newState = { | |
| x: event.pageX, | |
| y: event.pageY, | |
| }; | |
| if (ref.current?.nodeType === Node.ELEMENT_NODE) { | |
| const { left, top } = ref.current.getBoundingClientRect(); | |
| const elementPositionX = left + window.scrollX; | |
| const elementPositionY = top + window.scrollY; | |
| const elementX = event.pageX - elementPositionX; | |
| const elementY = event.pageY - elementPositionY; | |
| newState.elementX = elementX; | |
| newState.elementY = elementY; | |
| newState.elementPositionX = elementPositionX; | |
| newState.elementPositionY = elementPositionY; | |
| } | |
| setState((s) => { | |
| return { | |
| ...s, | |
| ...newState, | |
| }; | |
| }); | |
| }; | |
| document.addEventListener("mousemove", handleMouseMove); | |
| return () => { | |
| document.removeEventListener("mousemove", handleMouseMove); | |
| }; | |
| }, []); | |
| return [state, ref]; | |
| } | |
| const getConnection = () => { | |
| return ( | |
| navigator?.connection || | |
| navigator?.mozConnection || | |
| navigator?.webkitConnection | |
| ); | |
| }; | |
| const useNetworkStateSubscribe = (callback) => { | |
| window.addEventListener("online", callback, { passive: true }); | |
| window.addEventListener("offline", callback, { passive: true }); | |
| const connection = getConnection(); | |
| if (connection) { | |
| connection.addEventListener("change", callback, { passive: true }); | |
| } | |
| return () => { | |
| window.removeEventListener("online", callback); | |
| window.removeEventListener("offline", callback); | |
| if (connection) { | |
| connection.removeEventListener("change", callback); | |
| } | |
| }; | |
| }; | |
| const getNetworkStateServerSnapshot = () => { | |
| throw Error("useNetworkState is a client-only hook"); | |
| }; | |
| export function useNetworkState() { | |
| const cache = React.useRef({}); | |
| const getSnapshot = () => { | |
| const online = navigator.onLine; | |
| const connection = getConnection(); | |
| const nextState = { | |
| online, | |
| downlink: connection?.downlink, | |
| downlinkMax: connection?.downlinkMax, | |
| effectiveType: connection?.effectiveType, | |
| rtt: connection?.rtt, | |
| saveData: connection?.saveData, | |
| type: connection?.type, | |
| }; | |
| if (isShallowEqual(cache.current, nextState)) { | |
| return cache.current; | |
| } else { | |
| cache.current = nextState; | |
| return nextState; | |
| } | |
| }; | |
| return React.useSyncExternalStore( | |
| useNetworkStateSubscribe, | |
| getSnapshot, | |
| getNetworkStateServerSnapshot | |
| ); | |
| } | |
| export function useObjectState(initialValue) { | |
| const [state, setState] = React.useState(initialValue); | |
| const handleUpdate = React.useCallback((arg) => { | |
| if (typeof arg === "function") { | |
| setState((s) => { | |
| const newState = arg(s); | |
| if (isPlainObject(newState)) { | |
| return { | |
| ...s, | |
| ...newState, | |
| }; | |
| } | |
| }); | |
| } | |
| if (isPlainObject(arg)) { | |
| setState((s) => ({ | |
| ...s, | |
| ...arg, | |
| })); | |
| } | |
| }, []); | |
| return [state, handleUpdate]; | |
| } | |
| export function useOrientation() { | |
| const [orientation, setOrientation] = React.useState({ | |
| angle: 0, | |
| type: "landscape-primary", | |
| }); | |
| React.useLayoutEffect(() => { | |
| const handleChange = () => { | |
| const { angle, type } = window.screen.orientation; | |
| setOrientation({ | |
| angle, | |
| type, | |
| }); | |
| }; | |
| const handle_orientationchange = () => { | |
| setOrientation({ | |
| type: "UNKNOWN", | |
| angle: window.orientation, | |
| }); | |
| }; | |
| if (window.screen?.orientation) { | |
| handleChange(); | |
| window.screen.orientation.addEventListener("change", handleChange); | |
| } else { | |
| handle_orientationchange(); | |
| window.addEventListener("orientationchange", handle_orientationchange); | |
| } | |
| return () => { | |
| if (window.screen?.orientation) { | |
| window.screen.orientation.removeEventListener("change", handleChange); | |
| } else { | |
| window.removeEventListener( | |
| "orientationchange", | |
| handle_orientationchange | |
| ); | |
| } | |
| }; | |
| }, []); | |
| return orientation; | |
| } | |
| const usePreferredLanguageSubscribe = (cb) => { | |
| window.addEventListener("languagechange", cb); | |
| return () => window.removeEventListener("languagechange", cb); | |
| }; | |
| const getPreferredLanguageSnapshot = () => { | |
| return navigator.language; | |
| }; | |
| const getPreferredLanguageServerSnapshot = () => { | |
| throw Error("usePreferredLanguage is a client-only hook"); | |
| }; | |
| export function usePreferredLanguage() { | |
| return React.useSyncExternalStore( | |
| usePreferredLanguageSubscribe, | |
| getPreferredLanguageSnapshot, | |
| getPreferredLanguageServerSnapshot | |
| ); | |
| } | |
| export function usePrevious(value) { | |
| const [current, setCurrent] = React.useState(value); | |
| const [previous, setPrevious] = React.useState(null); | |
| if (value !== current) { | |
| setPrevious(current); | |
| setCurrent(value); | |
| } | |
| return previous; | |
| } | |
| export function useQueue(initialValue = []) { | |
| const [queue, setQueue] = React.useState(initialValue); | |
| const add = React.useCallback((element) => { | |
| setQueue((q) => [...q, element]); | |
| }, []); | |
| const remove = React.useCallback(() => { | |
| let removedElement; | |
| setQueue(([first, ...q]) => { | |
| removedElement = first; | |
| return q; | |
| }); | |
| return removedElement; | |
| }, []); | |
| const clear = React.useCallback(() => { | |
| setQueue([]); | |
| }, []); | |
| return { | |
| add, | |
| remove, | |
| clear, | |
| first: queue[0], | |
| last: queue[queue.length - 1], | |
| size: queue.length, | |
| queue, | |
| }; | |
| } | |
| export function useRenderCount() { | |
| const count = React.useRef(0); | |
| count.current++; | |
| return count.current; | |
| } | |
| export function useRenderInfo(name = "Unknown") { | |
| const count = React.useRef(0); | |
| const lastRender = React.useRef(); | |
| const now = Date.now(); | |
| count.current++; | |
| React.useEffect(() => { | |
| lastRender.current = Date.now(); | |
| }); | |
| const sinceLastRender = lastRender.current ? now - lastRender.current : 0; | |
| if (process.env.NODE_ENV !== "production") { | |
| const info = { | |
| name, | |
| renders: count.current, | |
| sinceLastRender, | |
| timestamp: now, | |
| }; | |
| console.log(info); | |
| return info; | |
| } | |
| } | |
| export function useScript(src, options = {}) { | |
| const [status, setStatus] = React.useState("loading"); | |
| const optionsRef = React.useRef(options); | |
| React.useEffect(() => { | |
| let script = document.querySelector(`script[src="${src}"]`); | |
| const domStatus = script?.getAttribute("data-status"); | |
| if (domStatus) { | |
| setStatus(domStatus); | |
| return; | |
| } | |
| if (script === null) { | |
| script = document.createElement("script"); | |
| script.src = src; | |
| script.async = true; | |
| script.setAttribute("data-status", "loading"); | |
| document.body.appendChild(script); | |
| const handleScriptLoad = () => { | |
| script.setAttribute("data-status", "ready"); | |
| setStatus("ready"); | |
| removeEventListeners(); | |
| }; | |
| const handleScriptError = () => { | |
| script.setAttribute("data-status", "error"); | |
| setStatus("error"); | |
| removeEventListeners(); | |
| }; | |
| const removeEventListeners = () => { | |
| script.removeEventListener("load", handleScriptLoad); | |
| script.removeEventListener("error", handleScriptError); | |
| }; | |
| script.addEventListener("load", handleScriptLoad); | |
| script.addEventListener("error", handleScriptError); | |
| const removeOnUnmount = optionsRef.current.removeOnUnmount; | |
| return () => { | |
| if (removeOnUnmount === true) { | |
| script.remove(); | |
| removeEventListeners(); | |
| } | |
| }; | |
| } else { | |
| setStatus("unknown"); | |
| } | |
| }, [src]); | |
| return status; | |
| } | |
| const setSessionStorageItem = (key, value) => { | |
| const stringifiedValue = JSON.stringify(value); | |
| window.sessionStorage.setItem(key, stringifiedValue); | |
| dispatchStorageEvent(key, stringifiedValue); | |
| }; | |
| const removeSessionStorageItem = (key) => { | |
| window.sessionStorage.removeItem(key); | |
| dispatchStorageEvent(key, null); | |
| }; | |
| const getSessionStorageItem = (key) => { | |
| return window.sessionStorage.getItem(key); | |
| }; | |
| const useSessionStorageSubscribe = (callback) => { | |
| window.addEventListener("storage", callback); | |
| return () => window.removeEventListener("storage", callback); | |
| }; | |
| const getSessionStorageServerSnapshot = () => { | |
| throw Error("useSessionStorage is a client-only hook"); | |
| }; | |
| export function useSessionStorage(key, initialValue) { | |
| const getSnapshot = () => getSessionStorageItem(key); | |
| const store = React.useSyncExternalStore( | |
| useSessionStorageSubscribe, | |
| getSnapshot, | |
| getSessionStorageServerSnapshot | |
| ); | |
| const setState = React.useCallback( | |
| (v) => { | |
| try { | |
| const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; | |
| if (nextState === undefined || nextState === null) { | |
| removeSessionStorageItem(key); | |
| } else { | |
| setSessionStorageItem(key, nextState); | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| } | |
| }, | |
| [key, store] | |
| ); | |
| React.useEffect(() => { | |
| if ( | |
| getSessionStorageItem(key) === null && | |
| typeof initialValue !== "undefined" | |
| ) { | |
| setSessionStorageItem(key, initialValue); | |
| } | |
| }, [key, initialValue]); | |
| return [store ? JSON.parse(store) : initialValue, setState]; | |
| } | |
| export function useSet(values) { | |
| const setRef = React.useRef(new Set(values)); | |
| const [, reRender] = React.useReducer((x) => x + 1, 0); | |
| setRef.current.add = (...args) => { | |
| const res = Set.prototype.add.apply(setRef.current, args); | |
| reRender(); | |
| return res; | |
| }; | |
| setRef.current.clear = (...args) => { | |
| Set.prototype.clear.apply(setRef.current, args); | |
| reRender(); | |
| }; | |
| setRef.current.delete = (...args) => { | |
| const res = Set.prototype.delete.apply(setRef.current, args); | |
| reRender(); | |
| return res; | |
| }; | |
| return setRef.current; | |
| } | |
| export function useThrottle(value, interval = 500) { | |
| const [throttledValue, setThrottledValue] = React.useState(value); | |
| const lastUpdated = React.useRef(null); | |
| React.useEffect(() => { | |
| const now = Date.now(); | |
| if (lastUpdated.current && now >= lastUpdated.current + interval) { | |
| lastUpdated.current = now; | |
| setThrottledValue(value); | |
| } else { | |
| const id = window.setTimeout(() => { | |
| lastUpdated.current = now; | |
| setThrottledValue(value); | |
| }, interval); | |
| return () => window.clearTimeout(id); | |
| } | |
| }, [value, interval]); | |
| return throttledValue; | |
| } | |
| export function useToggle(initialValue) { | |
| const [on, setOn] = React.useState(() => { | |
| if (typeof initialValue === "boolean") { | |
| return initialValue; | |
| } | |
| return Boolean(initialValue); | |
| }); | |
| const handleToggle = React.useCallback((value) => { | |
| if (typeof value === "boolean") { | |
| return setOn(value); | |
| } | |
| return setOn((v) => !v); | |
| }, []); | |
| return [on, handleToggle]; | |
| } | |
| const useVisibilityChangeSubscribe = (callback) => { | |
| document.addEventListener("visibilitychange", callback); | |
| return () => { | |
| document.removeEventListener("visibilitychange", callback); | |
| }; | |
| }; | |
| const getVisibilityChangeSnapshot = () => { | |
| return document.visibilityState; | |
| }; | |
| const getVisibilityChangeServerSnapshot = () => { | |
| throw Error("useVisibilityChange is a client-only hook"); | |
| }; | |
| export function useVisibilityChange() { | |
| const visibilityState = React.useSyncExternalStore( | |
| useVisibilityChangeSubscribe, | |
| getVisibilityChangeSnapshot, | |
| getVisibilityChangeServerSnapshot | |
| ); | |
| return visibilityState === "visible"; | |
| } | |
| export function useWindowScroll() { | |
| const [state, setState] = React.useState({ | |
| x: null, | |
| y: null, | |
| }); | |
| const scrollTo = React.useCallback((...args) => { | |
| if (typeof args[0] === "object") { | |
| window.scrollTo(args[0]); | |
| } else if (typeof args[0] === "number" && typeof args[1] === "number") { | |
| window.scrollTo(args[0], args[1]); | |
| } else { | |
| throw new Error( | |
| `Invalid arguments passed to scrollTo. See here for more info. https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo` | |
| ); | |
| } | |
| }, []); | |
| React.useLayoutEffect(() => { | |
| const handleScroll = () => { | |
| setState({ x: window.scrollX, y: window.scrollY }); | |
| }; | |
| handleScroll(); | |
| window.addEventListener("scroll", handleScroll); | |
| return () => { | |
| window.removeEventListener("scroll", handleScroll); | |
| }; | |
| }, []); | |
| return [state, scrollTo]; | |
| } | |
| export function useWindowSize() { | |
| const [size, setSize] = React.useState({ | |
| width: null, | |
| height: null, | |
| }); | |
| React.useLayoutEffect(() => { | |
| const handleResize = () => { | |
| setSize({ | |
| width: window.innerWidth, | |
| height: window.innerHeight, | |
| }); | |
| }; | |
| handleResize(); | |
| window.addEventListener("resize", handleResize); | |
| return () => { | |
| window.removeEventListener("resize", handleResize); | |
| }; | |
| }, []); | |
| return size; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment