UNPKG

@uidotdev/usehooks

Version:

A collection of modern, server-safe React hooks – from the ui.dev team

1,368 lines (1,102 loc) 32.2 kB
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; }