From 20d4fd8c61307dc11496b86738cdc42d31e42b46 Mon Sep 17 00:00:00 2001 From: Viki Date: Wed, 11 Sep 2024 16:35:39 +0800 Subject: [PATCH] feat(useInfiniteScroll): add `loadMore` method to load more manually & `reset` to reset, deprecated isLoading, prefer `loading` --- .../src/use-infinite-scroll/demo.tsx | 51 +++++++---- .../src/use-infinite-scroll/index.ts | 85 ++++++++++++------- 2 files changed, 91 insertions(+), 45 deletions(-) diff --git a/packages/react-use/src/use-infinite-scroll/demo.tsx b/packages/react-use/src/use-infinite-scroll/demo.tsx index e2372d2e..aa6cf3d3 100644 --- a/packages/react-use/src/use-infinite-scroll/demo.tsx +++ b/packages/react-use/src/use-infinite-scroll/demo.tsx @@ -1,45 +1,66 @@ -import { Card, KeyValue, Zone, wait as mockFetch } from '@/components' -import { generateLoremIpsum, useInfiniteScroll, useSafeState } from '@shined/react-use' +import { Button, Card, KeyValue, Zone, cn, wait as mockFetch } from '@/components' +import { generateLoremIpsum, useInfiniteScroll, useSafeState, useVersionedAction } from '@shined/react-use' import { useRef } from 'react' export function App() { const ref = useRef(null) const [list, setList] = useSafeState<{ idx: number; text: string }[]>([]) + const [incVersion, runVersionedAction] = useVersionedAction() const fetchData = async () => { - await mockFetch(Math.random() * 600 + 200) + const version = incVersion() - const newData = Array.from({ length: 20 }, (_, i) => ({ - idx: list.length + (i + 1), - text: generateLoremIpsum(), - })) + await mockFetch(1000) - setList([...list, ...newData]) + return await runVersionedAction(version, async () => { + const newData = Array.from({ length: 20 }, (_, i) => ({ + idx: list.length + (i + 1), + text: generateLoremIpsum(), + })) - return newData + setList([...list, ...newData]) + + return newData + }) } - const scroll = useInfiniteScroll(ref, fetchData, { + const infiniteScroll = useInfiniteScroll(ref, fetchData, { canLoadMore: (pre) => { return pre ? pre[pre.length - 1].idx <= 100 : true }, }) + const reset = () => { + incVersion() + setList([]) + infiniteScroll.reset() + } + return ( - - + + + -
+
{list.map((item) => (
{item.text}
))} - {scroll.isLoading &&
Loading...
} - {scroll.isLoadDone &&
No more data
} + {infiniteScroll.loading && ( +
Loading...
+ )} + {infiniteScroll.isLoadDone &&
No more data
}
+ ) } diff --git a/packages/react-use/src/use-infinite-scroll/index.ts b/packages/react-use/src/use-infinite-scroll/index.ts index e49a1266..23c93b76 100644 --- a/packages/react-use/src/use-infinite-scroll/index.ts +++ b/packages/react-use/src/use-infinite-scroll/index.ts @@ -2,10 +2,11 @@ import { useRef } from 'react' import { useEventListener } from '../use-event-listener' import { useLatest } from '../use-latest' import { useMount } from '../use-mount' -import { useRafState } from '../use-raf-state' import { useStableFn } from '../use-stable-fn' import { useTargetElement } from '../use-target-element' +import { useTrackedRefState } from '../use-tracked-ref-state' import { useUpdateEffect } from '../use-update-effect' +import { useVersionedAction } from '../use-versioned-action' import type { ElementTarget } from '../use-target-element' @@ -47,12 +48,26 @@ export interface UseInfiniteScrollOptions { export interface UseInfiniteScrollReturns { /** * Loading state + * + * @deprecated use `loading` instead */ isLoading: boolean + /** + * Loading state + * + * @since 1.7.0 + */ + loading: boolean /** * Load done state */ isLoadDone: boolean + /** + * Load more function + * + * @since 1.7.0 + */ + loadMore: () => Promise /** * Calculate the current scroll position to determine whether to load more */ @@ -60,11 +75,9 @@ export interface UseInfiniteScrollReturns { /** * Reset the state to the initial state. * - * @param {boolean} immediate Whether to trigger the first load immediately, default is true - * * @since 1.7.0 */ - reset(immediate?: boolean): void + reset(): void } /** @@ -86,16 +99,35 @@ export function useInfiniteScroll( const el = useTargetElement(target) const previousReturn = useRef(undefined) - const [state, setState] = useRafState({ isLoading: false, isLoadDone: false }, { deep: true }) + const [incVersion, runVersionedAction] = useVersionedAction() + const [state, { updateRefState }, stateRef] = useTrackedRefState({ + version: 0, + loading: false, + isLoadDone: false, + }) + const latest = useLatest({ state, canLoadMore, direction, onScroll, onLoadMore, interval }) - const calculate = useStableFn(async () => { - if (!latest.current.canLoadMore(previousReturn.current)) { - setState({ isLoadDone: true, isLoading: false }) - return - } + const loadMore = useStableFn(async () => { + if (stateRef.isLoadDone.value || stateRef.loading.value) return + + const version = incVersion() + updateRefState('loading', true) - if (!el.current || latest.current.state.isLoading) return + const [result, _] = await Promise.all([ + latest.current.onLoadMore(previousReturn.current), + new Promise((resolve) => setTimeout(resolve, latest.current.interval)), + ]) + + runVersionedAction(version, () => { + previousReturn.current = result + updateRefState('loading', false) + updateRefState('isLoadDone', !latest.current.canLoadMore(previousReturn.current)) + }) + }) + + const calculate = useStableFn(async () => { + if (!el.current || stateRef.isLoadDone.value || stateRef.loading.value) return const { scrollHeight, scrollTop, clientHeight, scrollWidth, clientWidth } = el.current @@ -107,27 +139,16 @@ export function useInfiniteScroll( : scrollWidth - scrollTop <= clientWidth + distance if (isScrollNarrower || isAlmostBottom) { - setState({ isLoadDone: false, isLoading: true }) - - const [result, _] = await Promise.all([ - latest.current.onLoadMore(previousReturn.current), - new Promise((resolve) => setTimeout(resolve, latest.current.interval)), - ]) - - previousReturn.current = result - - setState({ - isLoadDone: !latest.current.canLoadMore(previousReturn.current), - isLoading: false, - }) + await loadMore() } }) useMount(immediate && calculate) + // biome-ignore lint/correctness/useExhaustiveDependencies: need to detect `isLoadDone` change useUpdateEffect(() => { - if (!state.isLoading) calculate() - }, [state.isLoading]) + calculate() + }, [state.isLoadDone, state.loading, state.version]) useEventListener( el, @@ -139,15 +160,19 @@ export function useInfiniteScroll( { passive: true }, ) - const reset = useStableFn((immediate = true) => { + const reset = useStableFn(() => { previousReturn.current = undefined - setState({ isLoading: false, isLoadDone: false }) - immediate && calculate() + incVersion() + updateRefState('loading', false) + updateRefState('isLoadDone', false) + updateRefState('version', stateRef.version.value + 1) }) return { ...state, - calculate, + isLoading: state.loading, + loadMore, reset, + calculate, } }