web-api-hooks
Version:
Essential set of React Hooks for convenient Web API consumption.
693 lines (622 loc) • 20.9 kB
JavaScript
;
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