UNPKG

web-api-hooks

Version:

Essential set of React Hooks for convenient Web API consumption.

693 lines (622 loc) 20.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); const canUseDOM = typeof window !== 'undefined'; function dethunkify(value) { return typeof value === 'function' ? value() : value; } function managedEventListener(target, type, callback, options) { target.addEventListener(type, callback, options); return () => { target.removeEventListener(type, callback, options); }; } function managedInterval(callback, delayMs) { const id = setInterval(callback, delayMs); return () => { clearInterval(id); }; } function useEventCallback(callback) { // Source: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback const ref = react.useRef(); react.useEffect(() => { ref.current = callback; }, [callback]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return react.useCallback((...args) => ref.current(...args), [ref]); } /** * Tracks match state of a media query. * * @param query Media query to parse. * * @returns `true` if the associated media query list matches the state of the [`document`](https://developer.mozilla.org/docs/Web/API/Document), or `false` otherwise. * * @example * function Component() { * const isWidescreen = useMedia('(min-aspect-ratio: 16/9)'); * // ... * } */ function useMedia(query) { const [matches, setMatches] = react.useState(() => canUseDOM ? matchMedia(query).matches : false); react.useEffect(() => { const mediaQueryList = matchMedia(query); function handleChange() { setMatches(mediaQueryList.matches); } // Handle `query` param changes immediately handleChange(); // TODO: Refactor to `managedEventListener` when `change` event is supported mediaQueryList.addListener(handleChange); return () => { mediaQueryList.removeListener(handleChange); }; }, [query]); return matches; } /** * Tracks color scheme preference of the user. * * @returns Preferred color scheme. * * @example * function Component() { * const preferDarkMode = useColorSchemePreference() === 'dark'; * // ... * } */ function useColorSchemePreference() { const isDark = useMedia('(prefers-color-scheme: dark)'); if (isDark) return 'dark'; return 'light'; } const initialState = { acceleration: null, accelerationIncludingGravity: null, rotationRate: null, interval: 0 }; /** * Tracks acceleration and rotation rate of the device. * * @returns Own properties of the last corresponding event. * * @example * function Component() { * const { acceleration, rotationRate, interval } = useDeviceMotion(); * // ... * } */ function useDeviceMotion() { const [motion, setMotion] = react.useState(initialState); // TODO: Request permission if necessary, see https://github.com/w3c/deviceorientation/issues/57 react.useEffect(() => managedEventListener(window, 'devicemotion', event => { setMotion(event); }), []); return motion; } const initialState$1 = { alpha: null, beta: null, gamma: null, absolute: false }; /** * Tracks physical orientation of the device. * * @returns Own properties of the last corresponding event. * * @example * function Component() { * const { alpha, beta, gamma } = useDeviceOrientation(); * // ... * } */ function useDeviceOrientation() { const [orientation, setOrientation] = react.useState(initialState$1); react.useEffect(() => managedEventListener(window, 'deviceorientation', event => { setOrientation(event); }), []); return orientation; } /** * Tracks loading state of the page. * * @returns Readiness of the [`document`](https://developer.mozilla.org/docs/Web/API/Document), which is `'loading'` by default. * * @example * function Component() { * const documentReadiness = useDocumentReadiness(); * if (documentReadiness === 'interactive') { * // You may interact with any element of the document from now * } * // ... * } */ function useDocumentReadiness() { const [readiness, setReadiness] = react.useState(canUseDOM ? document.readyState : 'loading'); react.useEffect(() => managedEventListener(document, 'readystatechange', () => { setReadiness(document.readyState); }), []); return readiness; } /** * Tracks visibility of the page. * * @returns Visibility state of the [`document`](https://developer.mozilla.org/docs/Web/API/Document), which is `'visible'` by default. * * @example * function Component() { * const documentVisibility = useDocumentVisibility(); * if (documentVisibility === 'hidden') { * // Reduce resource utilization to aid background page performance * } * // ... * } */ function useDocumentVisibility() { const [visibility, setVisibility] = react.useState(canUseDOM ? document.visibilityState : 'visible'); react.useEffect(() => managedEventListener(document, 'visibilitychange', () => { setVisibility(document.visibilityState); }), []); return visibility; } /** * Listens to an event while the enclosing component is mounted. * * @see [Event reference on MDN](https://developer.mozilla.org/en-US/docs/Web/Events) * * @param {EventTarget} target Target to listen on, possibly a DOM element or a remote service connector. * @param {string} type Name of event (case-sensitive). * @param {EventListener} callback Method to execute whenever the event fires. * @param options Additional listener characteristics. * * @example * function Component() { * useEventListener(window, 'error', () => { * console.log('A resource failed to load.'); * }); * // ... * } */ function useEventListener(target, type, callback, options) { // Based on the implementation of `useInterval` const savedCallback = useEventCallback(callback); react.useEffect(() => managedEventListener(target, type, savedCallback, options), [options, savedCallback, target, type]); } /** * Tracks focus state of an element. * * @returns Whether the element has focus, and props to be spread over the element under observation. * * @example * function Component() { * const [isFocused, bindFocus] = useFocus(); * // ... * return <ElementToObserve {...bindFocus} />; * } */ function useFocus() { const [isFocused, setFocused] = react.useState(false); return [isFocused, { onFocus() { setFocused(true); }, onBlur() { setFocused(false); } }]; } /** * Tracks geolocation of the device. * * @param options Additional watching options. * @param errorCallback Method to execute in case of an error, e.g. when the user denies location sharing permissions. * @returns Locational data, or `undefined` when unavailable. * * @example * function Component() { * const geolocation = useGeolocation(); * if (geolocation) { * const { coords } = geolocation; * } * // ... * } */ function useGeolocation(options, errorCallback) { const [position, setPosition] = react.useState(); react.useEffect(() => { const id = navigator.geolocation.watchPosition(setPosition, errorCallback, options); return () => { navigator.geolocation.clearWatch(id); }; }, [errorCallback, options]); return position; } /** * Tracks hover state of an element. * * @param {boolean} disallowTouch Determines whether touch gestures should be ignored. * @returns Whether the element is hovered, and props to be spread over the element under observation. * * @example * function Component() { * const [isHovered, bindHover] = useHover(); * // ... * return <ElementToObserve {...bindHover} />; * } */ function useHover(disallowTouch = false) { const [isHovered, setHovered] = react.useState(false); return [isHovered, { onMouseEnter() { setHovered(true); }, onMouseLeave() { setHovered(false); }, onTouchStart() { setHovered(!disallowTouch); }, onTouchEnd() { setHovered(false); } }]; } /** * Repeatedly calls a function with a fixed time delay between each call. * * 📝 _Timings may be inherently inaccurate, due to the implementation of [`setInterval`](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) under the hood._ * * @param callback Method to execute periodically. * @param delayMs Time, in milliseconds, to wait between executions of the specified function. Set to `null` for pausing. * * @example * function Component() { * useInterval(() => { * // Custom logic to execute each second * }, 1000); * // ... * } */ function useInterval(callback, delayMs) { // Source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ const savedCallback = useEventCallback(callback); react.useEffect(() => delayMs != null ? managedInterval(savedCallback, delayMs) : undefined, [delayMs, savedCallback]); } function getPreferredLanguages() { return navigator.languages || [navigator.language]; } /** * Tracks language preferences of the user. * * @returns An array of [BCP 47](https://tools.ietf.org/html/bcp47) language tags, ordered by preference with the most preferred language first. * * @example * function Component() { * const preferredLanguages = useLanguagePreferences(); * // ... * } */ function useLanguagePreferences() { const [languages, setLanguages] = react.useState(canUseDOM ? getPreferredLanguages() : ['en-US', 'en']); react.useEffect(() => managedEventListener(window, 'languagechange', () => { setLanguages(getPreferredLanguages()); }), []); return languages; } function useStorage(getStorage, key, initialValue = null, errorCallback) { const storage = react.useMemo(() => { try { // Check if the storage object is defined and available // Prior to Firefox 70, localStorage may be null return getStorage(); // eslint-disable-next-line no-empty } catch (_unused) {} return null; }, [getStorage]); const [value, setValue] = react.useState(() => { const serializedValue = storage === null || storage === void 0 ? void 0 : storage.getItem(key); if (serializedValue == null) return dethunkify(initialValue); try { return JSON.parse(serializedValue); } catch (_unused2) { // Backwards compatibility with past stored non-serialized values return serializedValue; } }); react.useEffect(() => { if (storage) { try { storage.setItem(key, JSON.stringify(value)); } catch (error) { errorCallback === null || errorCallback === void 0 ? void 0 : errorCallback(error); } } }, [errorCallback, key, storage, value]); return [value, setValue]; } const getLocalStorage = () => localStorage; /** * Stores a key/value pair statefully in [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage). * * @see [`useState` hook](https://reactjs.org/docs/hooks-reference.html#usestate), which exposes a similar interface * * @param key Identifier to associate the stored value with. * @param initialValue Value used when no item exists with the given key. Lazy initialization is available by using a function which returns the desired value. * @param errorCallback Method to execute in case of an error, e.g. when the storage quota has been exceeded or trying to store a circular data structure. * @returns A statefully stored value, and a function to update it. * * @example * function Component() { * const [visitCount, setVisitCount] = useLocalStorage<number>('visitCount', 0); * useEffect(() => { * setVisitCount(count => count + 1); * }, []); * // ... * } */ function useLocalStorage(key, initialValue = null, errorCallback) { return useStorage(getLocalStorage, key, initialValue, errorCallback); } /** * Tracks motion intensity preference of the user. * * @returns Preferred motion intensity. * * @example * function Component() { * const preferReducedMotion = useMotionPreference() === 'reduce'; * // ... * } */ function useMotionPreference() { const isReduce = useMedia('(prefers-reduced-motion: reduce)'); if (isReduce) return 'reduce'; return 'no-preference'; } /** * Tracks mouse position. * * @returns Coordinates `[x, y]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const [mouseX, mouseY] = useMouseCoords(); * // ... * } */ function useMouseCoords() { const [coords, setCoords] = react.useState([0, 0]); react.useEffect(() => managedEventListener(window, 'mousemove', event => { setCoords([event.clientX, event.clientY]); }), []); return coords; } /** * Tracks information about the network's availability. * * ⚠️ _This attribute is inherently unreliable. A computer can be connected to a network without having internet access._ * * @returns `false` if the user agent is definitely offline, or `true` if it might be online. * * @example * function Component() { * const isOnline = useNetworkAvailability(); * // ... * } */ function useNetworkAvailability() { const [online, setOnline] = react.useState(canUseDOM ? navigator.onLine : true); react.useEffect(() => { const cleanup1 = managedEventListener(window, 'offline', () => { setOnline(false); }); const cleanup2 = managedEventListener(window, 'online', () => { setOnline(true); }); return () => { cleanup1(); cleanup2(); }; }, []); return online; } /** * Tracks information about the device's network connection. * * ⚗️ _The underlying technology is experimental. Please be aware about browser compatibility before using this in production._ * * @returns Connection data, or `undefined` when unavailable. * * @example * function Component() { * const networkInformation = useNetworkInformation(); * if (networkInformation) { * const { effectiveType, downlink, rtt, saveData } = networkInformation; * } * // ... * } */ function useNetworkInformation() { const [networkInformation, setNetworkInformation] = react.useState(canUseDOM ? navigator.connection : undefined); react.useEffect(() => navigator.connection ? managedEventListener(navigator.connection, 'change', () => { setNetworkInformation(navigator.connection); }) : undefined, []); return networkInformation; } const getSessionStorage = () => sessionStorage; /** * Stores a key/value pair statefully in [`sessionStorage`](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage). * * @see [`useState` hook](https://reactjs.org/docs/hooks-reference.html#usestate), which exposes a similar interface * * @param key Identifier to associate the stored value with. * @param initialValue Value used when no item exists with the given key. Lazy initialization is available by using a function which returns the desired value. * @param errorCallback Method to execute in case of an error, e.g. when the storage quota has been exceeded or trying to store a circular data structure. * @returns A statefully stored value, and a function to update it. * * @example * function Component() { * const [name, setName] = useSessionStorage<string>('name', 'Anonymous'); * // ... * } */ function useSessionStorage(key, initialValue = null, errorCallback) { return useStorage(getSessionStorage, key, initialValue, errorCallback); } /** * Tracks size of an element. * * ⚗️ _The underlying technology is experimental. Please be aware about browser compatibility before using this in production._ * * @param ref Attribute attached to the element under observation. * @param {TypeOf<ResizeObserver>} ResizeObserverOverride Replacement for `window.ResizeObserver`, e.g. [a polyfill](https://github.com/juggle/resize-observer). * * @returns Dimensions `[width, height]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const ref = useRef<HTMLElement>(null); * const [width, height] = useSize(ref); * // ... * return <ElementToObserve ref={ref} />; * } */ function useSize(ref, ResizeObserverOverride) { const [size, setSize] = react.useState([0, 0]); react.useEffect(() => { const ResizeObserver = ResizeObserverOverride || window.ResizeObserver; if (!ResizeObserver || !ref.current) return undefined; const observer = new ResizeObserver(([entry]) => { const { width, height } = entry.contentRect; setSize([width, height]); }); observer.observe(ref.current); return () => { observer.disconnect(); }; }, [ResizeObserverOverride, ref]); return size; } /** * Tracks visual viewport scale. * * ⚗️ _The underlying technology is experimental. Please be aware about browser compatibility before using this in production._ * * @returns Pinch-zoom scaling factor, falling back to `0` when unavailable. * * @example * function Component() { * const viewportScale = useViewportScale(); * // ... * } */ function useViewportScale() { const [scale, setScale] = react.useState(canUseDOM ? window.visualViewport.scale : 0); react.useEffect(() => managedEventListener(window.visualViewport, 'resize', () => { setScale(window.visualViewport.scale); }), []); return scale; } /** * Tracks visual viewport scroll position. * * ⚗️ _The underlying technology is experimental. Please be aware about browser compatibility before using this in production._ * * @returns Coordinates `[x, y]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const [viewportScrollX, viewportScrollY] = useViewportScrollCoords(); * // ... * } */ function useViewportScrollCoords() { const [coords, setCoords] = react.useState(canUseDOM ? [window.visualViewport.pageLeft, window.visualViewport.pageTop] : [0, 0]); react.useEffect(() => managedEventListener(window.visualViewport, 'scroll', () => { setCoords([window.visualViewport.pageLeft, window.visualViewport.pageTop]); }), []); return coords; } /** * Tracks visual viewport size. * * ⚗️ _The underlying technology is experimental. Please be aware about browser compatibility before using this in production._ * * @returns Dimensions `[width, height]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const [viewportWidth, viewportHeight] = useViewportSize(); * // ... * } */ function useViewportSize() { const [size, setSize] = react.useState(canUseDOM ? [window.visualViewport.width, window.visualViewport.height] : [0, 0]); react.useEffect(() => managedEventListener(window.visualViewport, 'resize', () => { setSize([window.visualViewport.width, window.visualViewport.height]); }), []); return size; } /** * Tracks window scroll position. * * @returns Coordinates `[x, y]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const [windowScrollX, windowScrollY] = useWindowScrollCoords(); * // ... * } */ function useWindowScrollCoords() { const [coords, setCoords] = react.useState(canUseDOM ? [window.pageXOffset, window.pageYOffset] : [0, 0]); react.useEffect(() => managedEventListener(window, 'scroll', () => { setCoords([window.pageXOffset, window.pageYOffset]); }), []); return coords; } /** * Tracks window size. * * @returns Dimensions `[width, height]`, falling back to `[0, 0]` when unavailable. * * @example * function Component() { * const [windowWidth, windowHeight] = useWindowSize(); * // ... * } */ function useWindowSize() { const [size, setSize] = react.useState(canUseDOM ? [window.innerWidth, window.innerHeight] : [0, 0]); react.useEffect(() => managedEventListener(window, 'resize', () => { setSize([window.innerWidth, window.innerHeight]); }), []); return size; } exports.useColorSchemePreference = useColorSchemePreference; exports.useDeviceMotion = useDeviceMotion; exports.useDeviceOrientation = useDeviceOrientation; exports.useDocumentReadiness = useDocumentReadiness; exports.useDocumentVisibility = useDocumentVisibility; exports.useEventListener = useEventListener; exports.useFocus = useFocus; exports.useGeolocation = useGeolocation; exports.useHover = useHover; exports.useInterval = useInterval; exports.useLanguagePreferences = useLanguagePreferences; exports.useLocalStorage = useLocalStorage; exports.useMedia = useMedia; exports.useMotionPreference = useMotionPreference; exports.useMouseCoords = useMouseCoords; exports.useNetworkAvailability = useNetworkAvailability; exports.useNetworkInformation = useNetworkInformation; exports.useSessionStorage = useSessionStorage; exports.useSize = useSize; exports.useViewportScale = useViewportScale; exports.useViewportScrollCoords = useViewportScrollCoords; exports.useViewportSize = useViewportSize; exports.useWindowScrollCoords = useWindowScrollCoords; exports.useWindowSize = useWindowSize; //# sourceMappingURL=index.js.map