UNPKG

ahooks

Version:
184 lines (183 loc) • 7.61 kB
import { __awaiter } from "tslib"; import { useMemo, useRef, useState } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import useEventListener from '../useEventListener'; import useMemoizedFn from '../useMemoizedFn'; import useRequest from '../useRequest'; import useUpdateEffect from '../useUpdateEffect'; import { getTargetElement } from '../utils/domTarget'; import { getClientHeight, getScrollHeight, getScrollTop } from '../utils/rect'; import useIsomorphicLayoutEffectWithTarget from '../utils/useIsomorphicLayoutEffectWithTarget'; const useInfiniteScroll = (service, options = {}) => { const { target, isNoMore, threshold = 100, direction = 'bottom', reloadDeps = [], manual, onBefore, onSuccess, onError, onFinally, } = options; const [finalData, setFinalData] = useState(); const [loadingMore, setLoadingMore] = useState(false); const isScrollToTop = direction === 'top'; // lastScrollTop is used to determine whether the scroll direction is up or down const lastScrollTop = useRef(undefined); // scrollBottom is used to record the distance from the bottom of the scroll bar const scrollBottom = useRef(0); // flag: set in onSuccess (bottom direction only) to trigger scrollMethod via effect const pendingBottomScrollCheckRef = useRef(false); const noMore = useMemo(() => { if (!isNoMore) { return false; } return isNoMore(finalData); }, [finalData]); const { loading, error, run, runAsync, cancel } = useRequest((lastData) => __awaiter(void 0, void 0, void 0, function* () { const currentData = yield service(lastData); return { currentData, lastData }; }), { manual, onFinally: (_, d, e) => { setLoadingMore(false); onFinally === null || onFinally === void 0 ? void 0 : onFinally(d === null || d === void 0 ? void 0 : d.currentData, e); }, onBefore: () => onBefore === null || onBefore === void 0 ? void 0 : onBefore(), onSuccess: (d) => { var _a, _b, _c, _d; if (!d.lastData) { setFinalData(Object.assign(Object.assign({}, d.currentData), { list: [...((_a = d.currentData.list) !== null && _a !== void 0 ? _a : [])] })); } else { setFinalData(Object.assign(Object.assign({}, d.currentData), { list: isScrollToTop ? [...d.currentData.list, ...((_b = d.lastData.list) !== null && _b !== void 0 ? _b : [])] : [...((_c = d.lastData.list) !== null && _c !== void 0 ? _c : []), ...d.currentData.list] })); } pendingBottomScrollCheckRef.current = !isScrollToTop && !!((_d = d.currentData.list) === null || _d === void 0 ? void 0 : _d.length); if (isScrollToTop) { setTimeout(() => { // use requestAnimationFrame to ensure the scroll position is updated (To ensure compatibility react 19) requestAnimationFrame(() => { let el = getTargetElement(target); el = el === document ? document.documentElement : el; if (el) { const scrollHeight = getScrollHeight(el); el.scrollTo(0, scrollHeight - scrollBottom.current); } }); }); } onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(d.currentData); }, onError: (e) => onError === null || onError === void 0 ? void 0 : onError(e), }); const loadMore = useMemoizedFn(() => { if (noMore) { return; } setLoadingMore(true); run(finalData); }); const runAsyncForCurrent = (data) => __awaiter(void 0, void 0, void 0, function* () { const res = yield runAsync(data); return res.currentData; }); const loadMoreAsync = useMemoizedFn(() => { if (noMore) { return Promise.reject(); } setLoadingMore(true); return runAsyncForCurrent(finalData); }); const reload = () => { setLoadingMore(false); return run(); }; const reloadAsync = () => { setLoadingMore(false); return runAsyncForCurrent(); }; const scrollMethod = useMemoizedFn(() => { if (loading || loadingMore) { return; } const el = getTargetElement(target); if (!el) { return; } const targetEl = el === document ? document.documentElement : el; const scrollTop = getScrollTop(targetEl); const scrollHeight = getScrollHeight(targetEl); const clientHeight = getClientHeight(targetEl); if (isScrollToTop) { if (lastScrollTop.current !== undefined && lastScrollTop.current > scrollTop && scrollTop <= threshold) { loadMore(); } lastScrollTop.current = scrollTop; scrollBottom.current = scrollHeight - scrollTop; } else if (scrollHeight - scrollTop <= clientHeight + threshold) { loadMore(); } }); useUpdateEffect(() => { if (!pendingBottomScrollCheckRef.current || loading || loadingMore) { return; } pendingBottomScrollCheckRef.current = false; scrollMethod(); }, [finalData, loading, loadingMore]); useEventListener('scroll', scrollMethod, { target }); // Re-check whether more data is needed when the scroll container suddenly gets taller const resizeCheck = useMemoizedFn(() => { // No successful data is available before a manual request or after the first load fails. // A resize should not trigger a request in those cases. if (!finalData) { return; } scrollMethod(); }); useIsomorphicLayoutEffectWithTarget(() => { const el = getTargetElement(target); if (!el) { return; } // The document's content box does not necessarily resize with the viewport. if (el === document) { window.addEventListener('resize', resizeCheck); return () => window.removeEventListener('resize', resizeCheck); } const targetEl = el; let clientWidth = targetEl.clientWidth; let clientHeight = targetEl.clientHeight; const observer = new ResizeObserver(() => { const nextWidth = targetEl.clientWidth; const nextHeight = targetEl.clientHeight; // observe() also notifies initially, even when the container has not resized. if (nextWidth === clientWidth && nextHeight === clientHeight) { return; } clientWidth = nextWidth; clientHeight = nextHeight; resizeCheck(); }); observer.observe(targetEl); return () => { observer.disconnect(); }; }, [], target); useUpdateEffect(() => { run(); }, [...reloadDeps]); return { data: finalData, loading: !loadingMore && loading, error, loadingMore, noMore, loadMore, loadMoreAsync, reload: useMemoizedFn(reload), reloadAsync: useMemoizedFn(reloadAsync), mutate: setFinalData, cancel: useMemoizedFn(() => { setLoadingMore(false); cancel(); }), }; }; export default useInfiniteScroll;