ahooks
Version:
react hooks library
145 lines (144 loc) • 5.75 kB
JavaScript
import { __awaiter } from "tslib";
import { useMemo, useRef, useState } from 'react';
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';
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;
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] }));
}
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);
}
});
});
}
else {
pendingBottomScrollCheckRef.current = true;
}
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 = () => {
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) {
return;
}
pendingBottomScrollCheckRef.current = false;
scrollMethod();
}, [finalData]);
useEventListener('scroll', () => {
if (loading || loadingMore) {
return;
}
scrollMethod();
}, { target });
useUpdateEffect(() => {
run();
}, [...reloadDeps]);
return {
data: finalData,
loading: !loadingMore && loading,
error,
loadingMore,
noMore,
loadMore,
loadMoreAsync,
reload: useMemoizedFn(reload),
reloadAsync: useMemoizedFn(reloadAsync),
mutate: setFinalData,
cancel,
};
};
export default useInfiniteScroll;