use-element-in-view
Version:
A simple React hook to track whether an element is visible in the viewport with the Intersection Observer. This API provides a native way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-leve
160 lines (134 loc) • 5.71 kB
JavaScript
import { useRef, useState, useMemo, useCallback, useEffect } from 'react';
function useLatest(value) {
var ref = useRef(value);
ref.current = value;
return ref;
}
function isRefObject(x) {
return x && typeof x === 'object' && 'current' in x;
}
function hasSupport() {
return typeof window !== 'undefined' && 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in IntersectionObserverEntry.prototype;
}
function useElementInView(_temp) {
var _ref = _temp === void 0 ? {} : _temp,
_ref$ref = _ref.ref,
forwardedRef = _ref$ref === void 0 ? null : _ref$ref,
_ref$root = _ref.root,
root = _ref$root === void 0 ? null : _ref$root,
_ref$rootMargin = _ref.rootMargin,
rootMargin = _ref$rootMargin === void 0 ? '0px' : _ref$rootMargin,
_ref$threshold = _ref.threshold,
threshold = _ref$threshold === void 0 ? 0 : _ref$threshold,
_ref$defaultInView = _ref.defaultInView,
defaultInView = _ref$defaultInView === void 0 ? false : _ref$defaultInView,
_ref$disconnectOnceVi = _ref.disconnectOnceVisible,
disconnectOnceVisible = _ref$disconnectOnceVi === void 0 ? false : _ref$disconnectOnceVi,
onChange = _ref.onChange;
var _useState = useState({
entry: undefined,
elementInView: defaultInView
}),
observerEntry = _useState[0],
setObserverEntry = _useState[1]; // Store our Intersection Observer instance to a ref for its lifecycle.
var observerInstanceRef = useRef(null); // Element refs. We store the previously tracked element to ensure we only update when the element has changed
// Along with the element supplied via the callback ref.
var callbackElementRef = useRef(null);
var prevTrackedElementRef = useRef(null); // Helper refs.
var isObservingRef = useRef(false);
var onChangeRef = useLatest(onChange); // Store the threshold as a primitive value to ensure it doesn't change in the deps array
// for registerObserver fn when the consumer passes in an inline array.
// eg: threshold: [0.25, 0.5] => will be diffed as a new array each render
var memoizedThreshold = useMemo(function () {
return Array.isArray(threshold) ? threshold.join() : threshold;
}, [threshold]);
var observeElement = useCallback(function (node) {
if (isObservingRef.current || !observerInstanceRef.current) {
return;
}
isObservingRef.current = true;
observerInstanceRef.current.observe(node);
}, []);
var disconnect = useCallback(function () {
if (!isObservingRef.current || !observerInstanceRef.current) {
return;
}
observerInstanceRef.current.disconnect(); // clear all refs
observerInstanceRef.current = null;
isObservingRef.current = false;
prevTrackedElementRef.current = null;
}, []); // Instantiates the Intersection Observer.
// It will determine the element based off how it was provided
// and return a warning if no element was found via the options to assign one.
var registerObserver = useCallback(function () {
if (!hasSupport()) {
return;
}
var element = null;
if (callbackElementRef.current) {
element = callbackElementRef.current;
} else if (forwardedRef instanceof HTMLElement) {
element = forwardedRef;
} else if (isRefObject(forwardedRef)) {
element = forwardedRef.current;
} // Don't update or recall the register function unless the element has changed.
if (prevTrackedElementRef.current === element) return;
prevTrackedElementRef.current = element;
if (!element) {
// eslint-disable-next-line
console.warn('🚨 No element has been found, are you sure you correctly assigned a ref?');
return;
} // Take our stored threshold and transform it to `number | number[]` that is required
// for the IntersectionObserverInit option
var transformedThreshold = typeof memoizedThreshold === 'string' ? memoizedThreshold.split(',').map(function (value) {
return parseFloat(value);
}) : memoizedThreshold; // Ensure we only create the instance once
if (!observerInstanceRef.current) {
var instance = new IntersectionObserver(function (_ref2) {
var entry = _ref2[0];
var isIntersecting = entry.isIntersecting && instance.thresholds.some(function (threshold) {
return entry.intersectionRatio >= threshold;
}); // Disconnect the instance once the observed entry is in view, and option to disconnectOnceVisible has been set
if (isIntersecting && disconnectOnceVisible) {
disconnect();
}
if (onChangeRef.current) {
onChangeRef.current(entry);
} else {
setObserverEntry({
entry: entry,
elementInView: isIntersecting
});
}
}, {
root: root,
rootMargin: rootMargin,
threshold: transformedThreshold
});
observerInstanceRef.current = instance;
}
observeElement(element);
}, [root, rootMargin, memoizedThreshold, disconnectOnceVisible, observeElement, disconnect, forwardedRef, onChangeRef]);
var callbackRef = useCallback(function (node) {
if (node) {
callbackElementRef.current = node;
registerObserver();
}
}, [registerObserver]);
useEffect(function () {
if (forwardedRef) {
registerObserver();
}
return function () {
disconnect();
};
}, [forwardedRef, registerObserver, disconnect]);
return {
entry: observerEntry.entry,
inView: observerEntry.elementInView,
assignRef: callbackRef,
disconnect: disconnect
};
}
export { useElementInView };
//# sourceMappingURL=use-element-in-view.esm.js.map