From d7cf6411b9d36fc21b8fe33e36508b74f7de3af9 Mon Sep 17 00:00:00 2001 From: jingzouzou <827088092@qq.com> Date: Wed, 4 Sep 2024 23:50:08 +0800 Subject: [PATCH 1/2] feat: support userInfiniteScroll use forcedLoadMore to keep requesting when data is no more --- .../hooks/src/useInfiniteScroll/index.tsx | 40 ++++++++++++++----- packages/hooks/src/useInfiniteScroll/types.ts | 4 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/hooks/src/useInfiniteScroll/index.tsx b/packages/hooks/src/useInfiniteScroll/index.tsx index a559c562d1..4623ae7049 100644 --- a/packages/hooks/src/useInfiniteScroll/index.tsx +++ b/packages/hooks/src/useInfiniteScroll/index.tsx @@ -21,18 +21,28 @@ const useInfiniteScroll = ( onSuccess, onError, onFinally, + forcedLoadMore = false, } = options; const [finalData, setFinalData] = useState(); const [loadingMore, setLoadingMore] = useState(false); - const noMore = useMemo(() => { - if (!isNoMore) return false; - return isNoMore(finalData); + const { noMore, trulyNoMore } = useMemo(() => { + if (!isNoMore) + return { + noMore: false, + trulyNoMore: false, + }; + const dataIsNoMore = isNoMore(finalData); + + return { + noMore: !forcedLoadMore && dataIsNoMore, + trulyNoMore: dataIsNoMore, + }; }, [finalData]); const { loading, error, run, runAsync, cancel } = useRequest( - async (lastData?: TData) => { + async (lastData?: TData, _isNeedForcedLoadMore?: boolean) => { const currentData = await service(lastData); if (!lastData) { setFinalData({ @@ -54,8 +64,12 @@ const useInfiniteScroll = ( onFinally?.(d, e); }, onBefore: () => onBefore?.(), - onSuccess: (d) => { + onSuccess: (d, isNeedForcedLoadMore) => { setTimeout(() => { + // if request trrigered by forced load more, is no need to check again + if (isNeedForcedLoadMore) { + return; + } // eslint-disable-next-line @typescript-eslint/no-use-before-define scrollMethod(); }); @@ -65,16 +79,16 @@ const useInfiniteScroll = ( }, ); - const loadMore = useMemoizedFn(() => { + const loadMore = useMemoizedFn((isNeedForcedLoadMore = false) => { if (noMore) return; setLoadingMore(true); - run(finalData); + run(finalData, isNeedForcedLoadMore); }); - const loadMoreAsync = useMemoizedFn(() => { + const loadMoreAsync = useMemoizedFn((isNeedForcedLoadMore = false) => { if (noMore) return Promise.reject(); setLoadingMore(true); - return runAsync(finalData); + return runAsync(finalData, isNeedForcedLoadMore); }); const reload = () => { @@ -99,8 +113,11 @@ const useInfiniteScroll = ( const scrollHeight = getScrollHeight(el); const clientHeight = getClientHeight(el); - if (scrollHeight - scrollTop <= clientHeight + threshold) { + // when set `forcedLoadMore`, only user scroll to bottom will trrigger load more + if (!forcedLoadMore && scrollHeight - scrollTop <= clientHeight + threshold) { loadMore(); + } else if (scrollHeight - scrollTop <= clientHeight) { + loadMore(true); } }; @@ -125,7 +142,8 @@ const useInfiniteScroll = ( error, loadingMore, noMore, - + /** when set `forcedLoadMore` true, `noMore` will be always true, use `trulyNoMore` to judge data whther is no more */ + trulyNoMore, loadMore, loadMoreAsync, reload: useMemoizedFn(reload), diff --git a/packages/hooks/src/useInfiniteScroll/types.ts b/packages/hooks/src/useInfiniteScroll/types.ts index cc17a688e4..d472067819 100644 --- a/packages/hooks/src/useInfiniteScroll/types.ts +++ b/packages/hooks/src/useInfiniteScroll/types.ts @@ -11,7 +11,8 @@ export interface InfiniteScrollResult { loadingMore: boolean; error?: Error; noMore: boolean; - + /** when set `forcedLoadMore` true, `noMore` will be always true, use `trulyNoMore` to judge data whther is no more */ + trulyNoMore: boolean; loadMore: () => void; loadMoreAsync: () => Promise; reload: () => void; @@ -26,6 +27,7 @@ export interface InfiniteScrollOptions { threshold?: number; manual?: boolean; + forcedLoadMore?: boolean; reloadDeps?: DependencyList; onBefore?: () => void; From f58c6cf1f0d51ac523ee64224d8be4691b0a56c1 Mon Sep 17 00:00:00 2001 From: jingzouzou <827088092@qq.com> Date: Sun, 22 Sep 2024 19:44:21 +0800 Subject: [PATCH 2/2] chore: add forcedLoadMore test cases and readme --- .../useInfiniteScroll/__tests__/index.test.ts | 100 ++++++++++++++++++ .../demo/scrollForcedLoadMore.tsx | 72 +++++++++++++ .../src/useInfiniteScroll/index.en-US.md | 8 ++ .../hooks/src/useInfiniteScroll/index.tsx | 24 +++-- .../src/useInfiniteScroll/index.zh-CN.md | 9 ++ 5 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 packages/hooks/src/useInfiniteScroll/demo/scrollForcedLoadMore.tsx diff --git a/packages/hooks/src/useInfiniteScroll/__tests__/index.test.ts b/packages/hooks/src/useInfiniteScroll/__tests__/index.test.ts index 12d0c8654b..7b6ef5cec5 100644 --- a/packages/hooks/src/useInfiniteScroll/__tests__/index.test.ts +++ b/packages/hooks/src/useInfiniteScroll/__tests__/index.test.ts @@ -18,6 +18,7 @@ export async function mockRequest() { } const targetEl = document.createElement('div'); +const scrollToBottomEl = document.createElement('div'); const setup = (service: Service, options?: InfiniteScrollOptions) => renderHook(() => useInfiniteScroll(service, options)); @@ -125,6 +126,105 @@ describe('useInfiniteScroll', () => { mockAddEventListener.mockRestore(); }); + it('should not be trriggerd when not scroll to bottom and configured with "forcedLoadMore"', async () => { + const events = {}; + const mockAddEventListener = jest + .spyOn(targetEl, 'addEventListener') + .mockImplementation((eventName, callback) => { + events[eventName] = callback; + }); + const { result } = setup(mockRequest, { + target: targetEl, + isNoMore: (d) => d?.nextId === undefined, + forcedLoadMore: true, + }); + // not work when loading + expect(result.current.loading).toBe(true); + events['scroll'](); + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current.loading).toBe(false); + + // mock scroll + Object.defineProperties(targetEl, { + clientHeight: { + value: 150, + }, + scrollHeight: { + value: 300, + }, + scrollTop: { + value: 100, + }, + }); + + act(() => { + events['scroll'](); + }); + // should not be trriggerd when not scroll to bottom + expect(result.current.loadingMore).toBe(false); + mockAddEventListener.mockRestore(); + }); + + it('should always load when configured with "forcedLoadMore"', async () => { + const events = {}; + const mockAddEventListener = jest + .spyOn(scrollToBottomEl, 'addEventListener') + .mockImplementation((eventName, callback) => { + events[eventName] = callback; + }); + const { result } = setup(mockRequest, { + target: scrollToBottomEl, + isNoMore: (d) => d?.nextId === undefined, + forcedLoadMore: true, + }); + // not work when loading + expect(result.current.loading).toBe(true); + events['scroll'](); + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current.loading).toBe(false); + + // mock scroll + Object.defineProperties(scrollToBottomEl, { + clientHeight: { + value: 150, + }, + scrollHeight: { + value: 300, + }, + scrollTop: { + value: 150, + }, + }); + + act(() => { + events['scroll'](); + }); + expect(result.current.loadingMore).toBe(true); + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current.loadingMore).toBe(false); + + // still work when no more + expect(result.current.noMore).toBe(false); + expect(result.current.trulyNoMore).toBe(true); + act(() => { + events['scroll'](); + }); + + expect(result.current.loadingMore).toBe(true); + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current.loadingMore).toBe(false); + + mockAddEventListener.mockRestore(); + }); + it('reload should be work', async () => { const fn = jest.fn(() => Promise.resolve({ list: [] })); const { result } = setup(fn); diff --git a/packages/hooks/src/useInfiniteScroll/demo/scrollForcedLoadMore.tsx b/packages/hooks/src/useInfiniteScroll/demo/scrollForcedLoadMore.tsx new file mode 100644 index 0000000000..c7b810171b --- /dev/null +++ b/packages/hooks/src/useInfiniteScroll/demo/scrollForcedLoadMore.tsx @@ -0,0 +1,72 @@ +import React, { useRef } from 'react'; +import { useInfiniteScroll } from 'ahooks'; + +interface Result { + list: string[]; + nextId: string | undefined; + lastId: string | undefined; +} + +const resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13']; + +setInterval(() => { + resultData.push(resultData.length.toString()); +}, 5000); + +function getLoadMoreList(lastId: string | undefined, limit: number): Promise { + let start = -1; + if (lastId) { + start = resultData.findIndex((i) => i === lastId); + } + const end = start + limit; + const list = resultData.slice(start + 1, end); + const nId = resultData.length >= end ? resultData[end] : undefined; + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + list, + nextId: nId, + lastId: list[list.length - 1] || lastId, + }); + }, 1000); + }); +} + +export default () => { + const ref = useRef(null); + + const { data, loading, loadMore, loadingMore, noMore, trulyNoMore } = useInfiniteScroll( + (d) => getLoadMoreList(d?.lastId, 4), + { + target: ref, + isNoMore: (d) => d?.nextId === undefined, + forcedLoadMore: true, + }, + ); + + return ( +
+ {loading ? ( +

loading

+ ) : ( +
+ {data?.list?.map((item) => ( +
+ item-{item} +
+ ))} +
+ )} + +
+ {!noMore && ( + + )} + + {trulyNoMore && No more data} +
+
+ ); +}; diff --git a/packages/hooks/src/useInfiniteScroll/index.en-US.md b/packages/hooks/src/useInfiniteScroll/index.en-US.md index b26f5f1f46..01220b9f95 100644 --- a/packages/hooks/src/useInfiniteScroll/index.en-US.md +++ b/packages/hooks/src/useInfiniteScroll/index.en-US.md @@ -39,6 +39,14 @@ In the infinite scrolling scenario, the most common case is to automatically loa +## Scroll Forced Infinite Loading + +When there is no more data, but the user still wants to try loading potentially updated data, you can configure `options.forcedLoadMore` for forced loading. + +- `options.forcedLoadMore` When `options.isNoMore` is `true`, scrolling to the bottom will still trigger forced loading. +- `options.trulyNoMore` When `options.forcedLoadMore` is set to `true`, `options.isNoMore` will remain `true`, so you need to use `options.trulyNoMore` to determine if there is truly no more data. + + ## Data reset The data can be reset by `reload`. The following example shows that after the `filter` changes, the data is reset to the first page. diff --git a/packages/hooks/src/useInfiniteScroll/index.tsx b/packages/hooks/src/useInfiniteScroll/index.tsx index 4623ae7049..298953cdf0 100644 --- a/packages/hooks/src/useInfiniteScroll/index.tsx +++ b/packages/hooks/src/useInfiniteScroll/index.tsx @@ -79,16 +79,22 @@ const useInfiniteScroll = ( }, ); - const loadMore = useMemoizedFn((isNeedForcedLoadMore = false) => { + const loadMore = useMemoizedFn((isScrollToBottom = false) => { if (noMore) return; + // when set `forcedLoadMore`, only user scroll to bottom will trrigger load more + if (forcedLoadMore && !isScrollToBottom) return; + setLoadingMore(true); - run(finalData, isNeedForcedLoadMore); + run(finalData, forcedLoadMore && isScrollToBottom); }); - const loadMoreAsync = useMemoizedFn((isNeedForcedLoadMore = false) => { + const loadMoreAsync = useMemoizedFn((isScrollToBottom = false) => { if (noMore) return Promise.reject(); + // when set `forcedLoadMore`, only user scroll to bottom will trrigger load more + if (forcedLoadMore && !isScrollToBottom) return Promise.reject(); + setLoadingMore(true); - return runAsync(finalData, isNeedForcedLoadMore); + return runAsync(finalData, forcedLoadMore && isScrollToBottom); }); const reload = () => { @@ -113,11 +119,13 @@ const useInfiniteScroll = ( const scrollHeight = getScrollHeight(el); const clientHeight = getClientHeight(el); - // when set `forcedLoadMore`, only user scroll to bottom will trrigger load more - if (!forcedLoadMore && scrollHeight - scrollTop <= clientHeight + threshold) { + if (scrollHeight - scrollTop <= clientHeight + threshold) { + if (scrollHeight - scrollTop <= clientHeight) { + loadMore(true); + return; + } + loadMore(); - } else if (scrollHeight - scrollTop <= clientHeight) { - loadMore(true); } }; diff --git a/packages/hooks/src/useInfiniteScroll/index.zh-CN.md b/packages/hooks/src/useInfiniteScroll/index.zh-CN.md index f9483407e2..c597492413 100644 --- a/packages/hooks/src/useInfiniteScroll/index.zh-CN.md +++ b/packages/hooks/src/useInfiniteScroll/index.zh-CN.md @@ -39,6 +39,15 @@ useInfiniteScroll 的第一个参数 `service` 是一个异步函数,对这个 +## 滚动强制无限加载 + +当没有更多数据时,用户依旧想尝试加载可能更新的数据,可以配置`options.forcedLoadMore`来进行强制加载。 + +- `options.forcedLoadMore` 当 `options.isNoMore`为`true`时滚动到底部依旧强制加载 +- `options.trulyNoMore` 当配置`options.forcedLoadMore`为`true`时, `options.isNoMore`为`true`一直为`true`,需要使用`options.trulyNoMore`来判断是不是没有更多数据了 + + + ## 数据重置 通过 `reload` 即可实现数据重置,下面示例我们演示在 `filter` 变化后,重置数据到第一页。