Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 18x 18x 18x 18x 18x 18x 2x 2x 2x 18x 18x 18x 14x 13x 1x 18x 16x 1x 1x 1x 18x 18x 14x 18x 18x 13x 18x 18x 18x | import {
useState,
useEffect,
useRef,
useMemo,
useCallback,
type ReactNode,
type ComponentType,
} from 'react';
import ButtonComponent from './button';
import Loader from './loader';
import '../styles/components/data-loader.scss';
export type WrapperProps<D> = {
/**
* Callback to request more items if user scrolled to the bottom of the scroll-container or if
* the scroll-container isn't scrollable yet because not enough items have been loaded yet. If
* not provided this component will simply pass the data prop to the BaseComponent to be rendered
* without observing scroll or triggering more data loading.
*/
onLoadMoreItems: () => void;
/**
* A boolean to indicate that the parent has more items to provide.
*/
hasMoreData: boolean;
/**
* A custom loader component
*/
loaderComponent?: ReactNode;
/**
* Data that is being represented in the wrapped component
*/
data: D[];
/**
* Use a button to load more data instead of having infinite scrolling.
* If this prop is a string or a node, it will render this within the button
*/
clickToLoad?: boolean | ReactNode;
};
type BaseComponentProps<D> = {
data: D[];
};
function withDataLoader<
// data type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
D extends Record<string, any>,
// props types of wrapped component
P extends BaseComponentProps<D>,
>(BaseComponent: ComponentType<P>) {
const Wrapper: ComponentType<P & WrapperProps<P['data'][0]>> = ({
onLoadMoreItems,
hasMoreData,
loaderComponent = <Loader />,
data,
clickToLoad = false,
...props
}) => {
// store this prop in a ref to not trigger re-creation of observer if the user
// of this component forgot to memoize 'onLoadMoreItems' function.
const onLoadMoreItemsRef = useRef(onLoadMoreItems);
onLoadMoreItemsRef.current = onLoadMoreItems;
const [loading, setLoading] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);
const handleAskForMoreData = useCallback(() => {
setLoading(true);
Eif (onLoadMoreItemsRef.current) {
onLoadMoreItemsRef.current();
}
}, []);
const observerCallbackRef = useRef<
((entry: IntersectionObserverEntry) => void) | null
>(null);
observerCallbackRef.current = ({ isIntersecting }) => {
if (!isIntersecting || loading || !hasMoreData) {
// skip if
// - "Loading..." component not visible;
// - currently loading more data; or
// - no more data available
return;
}
handleAskForMoreData();
};
const observer = useMemo(() => {
if (!('IntersectionObserver' in window) || clickToLoad) {
return;
}
// eslint-disable-next-line consistent-return
return new window.IntersectionObserver(([entry]) => {
// use it inside an other function, otherwise will use the first version
if (observerCallbackRef.current) {
observerCallbackRef.current(entry);
}
});
}, [clickToLoad]);
// eslint-disable-next-line consistent-return
useEffect(() => {
if (sentinelRef.current && observer && !loading) {
const element = sentinelRef.current;
observer.observe(element);
return () => observer.unobserve(element);
}
}, [observer, loading]);
// reset loading flag when data length changes
const length = data?.length;
useEffect(() => {
setLoading(false);
}, [length]);
let sentinelContent = loaderComponent;
if ((!('IntersectionObserver' in window) || clickToLoad) && !loading) {
sentinelContent = (
<ButtonComponent
variant="secondary"
onClick={handleAskForMoreData}
data-testid="click-to-load-more"
>
{(typeof clickToLoad === 'string' && clickToLoad) || 'Load more data'}
</ButtonComponent>
);
}
// TS doesn't like when I separate data from props when asserting as P
const baseComponentProps = { ...props, data };
return (
<>
<BaseComponent {...(baseComponentProps as unknown as P)} />
<div className="data-loader__loading" ref={sentinelRef}>
{hasMoreData && sentinelContent}
</div>
</>
);
};
return Wrapper;
}
export default withDataLoader;
|