Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/hooks/src/useInfiniteScroll/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,4 +532,84 @@ describe('useInfiniteScroll', () => {
scrollHeightSpy.mockRestore();
clientHeightSpy.mockRestore();
});

test('should auto loadMore after initial load when container is not full, and second request receives the latest finalData', async () => {
setTargetInfo('scrollTop', 0);
const scrollHeightSpy = vi.spyOn(targetEl, 'scrollHeight', 'get').mockImplementation(() => 50);
const clientHeightSpy = vi.spyOn(targetEl, 'clientHeight', 'get').mockImplementation(() => 300);

let capturedLastData: any;
const service = vi.fn(async (lastData?: any) => {
capturedLastData = lastData;
await sleep(1000);
if (!lastData) {
return { list: [1, 2, 3], nextId: 1 };
}
return { list: [4, 5, 6] };
});

const { result } = setup(service, {
target: targetEl,
isNoMore: (d) => d?.nextId === undefined,
});

// Wait for initial load to complete
await act(async () => {
vi.advanceTimersByTime(1000);
});

// Container is not full, so scrollMethod should have auto-triggered loadMore
expect(result.current.loadingMore).toBe(true);
// The second call must receive the up-to-date finalData (not the stale closure value)
expect(capturedLastData).toMatchObject({ list: [1, 2, 3], nextId: 1 });

await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(result.current.data?.list).toEqual([1, 2, 3, 4, 5, 6]);
expect(result.current.noMore).toBe(true);

scrollHeightSpy.mockRestore();
clientHeightSpy.mockRestore();
});

test('mutate should not trigger extra loadMore', async () => {
setTargetInfo('scrollTop', 0);
const scrollHeightSpy = vi.spyOn(targetEl, 'scrollHeight', 'get').mockImplementation(() => 50);
const clientHeightSpy = vi.spyOn(targetEl, 'clientHeight', 'get').mockImplementation(() => 300);

const service = vi.fn(mockRequest);
const { result } = setup(service, {
target: targetEl,
isNoMore: (d) => d?.nextId === undefined,
});

// Wait for initial load to complete; this auto-triggers a second load
await act(async () => {
vi.advanceTimersByTime(1000);
});
// Wait for the auto-triggered second load to complete
await act(async () => {
vi.advanceTimersByTime(1000);
});
expect(result.current.noMore).toBe(true);

const callCountAfterLoad = service.mock.calls.length;

// Mutate with data that makes noMore false, so loadMore *would* fire if scrollMethod ran
act(() => {
result.current.mutate({ list: [10, 20], nextId: 1 });
});

// Wait for any effects to run
await act(async () => {
vi.advanceTimersByTime(100);
});

// mutate should NOT trigger additional requests via scrollMethod
expect(service).toBeCalledTimes(callCountAfterLoad);

scrollHeightSpy.mockRestore();
clientHeightSpy.mockRestore();
});
});
27 changes: 17 additions & 10 deletions packages/hooks/src/useInfiniteScroll/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const useInfiniteScroll = <TData extends Data>(
const lastScrollTop = useRef<number>(undefined);
// scrollBottom is used to record the distance from the bottom of the scroll bar
const scrollBottom = useRef<number>(0);
// flag: set in onSuccess (bottom direction only) to trigger scrollMethod via effect
const pendingBottomScrollCheckRef = useRef(false);

const noMore = useMemo(() => {
if (!isNoMore) {
Expand Down Expand Up @@ -66,23 +68,21 @@ const useInfiniteScroll = <TData extends Data>(
});
}

setTimeout(() => {
// use requestAnimationFrame to ensure the scroll position is updated (To ensure compatibility react 19)
requestAnimationFrame(() => {
if (isScrollToTop) {
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 as Element).scrollTo(0, scrollHeight - scrollBottom.current);
}
} else {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
scrollMethod();
}
});
});
Comment thread
bowencool marked this conversation as resolved.
});

} else {
pendingBottomScrollCheckRef.current = true;
}
onSuccess?.(d.currentData);
},
onError: (e) => onError?.(e),
Expand Down Expand Up @@ -145,6 +145,13 @@ const useInfiniteScroll = <TData extends Data>(
loadMore();
}
};
useUpdateEffect(() => {
if (!pendingBottomScrollCheckRef.current) {
return;
}
pendingBottomScrollCheckRef.current = false;
scrollMethod();
Comment thread
bowencool marked this conversation as resolved.
}, [finalData]);
Comment thread
bowencool marked this conversation as resolved.
Comment thread
bowencool marked this conversation as resolved.

useEventListener(
'scroll',
Expand Down
Loading