Skip to content

Commit

Permalink
feat(useInfiniteScroll): add loadMore method to load more manually …
Browse files Browse the repository at this point in the history
…& `reset` to reset, deprecated isLoading, prefer `loading`
  • Loading branch information
vikiboss committed Sep 11, 2024
1 parent 1538cf9 commit 20d4fd8
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 45 deletions.
51 changes: 36 additions & 15 deletions packages/react-use/src/use-infinite-scroll/demo.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<Card>
<Zone>
<KeyValue label="isLoading" value={scroll.isLoading} />
<KeyValue label="isLoadDone" value={scroll.isLoadDone} />
<KeyValue label="Loading" value={infiniteScroll.loading} />
<KeyValue label="isLoadDone" value={infiniteScroll.isLoadDone} />
<KeyValue label="Item Count" value={list.length} />
</Zone>
<div ref={ref} className="w-full h-80 bg-#666666/20 rounded overflow-scroll p-4">
<div
ref={ref}
className={cn(
'w-full h-80 bg-#666666/20 rounded overflow-scroll p-4 transition-all',
infiniteScroll.loading ? 'opacity-60' : '',
)}
>
{list.map((item) => (
<div key={item.idx} className="my-2 p-2 bg-primary/40 dark:text-white rounded">
{item.text}
</div>
))}
{scroll.isLoading && <div className="text-center my-1 py-1 dark:text-white animate-pulse">Loading...</div>}
{scroll.isLoadDone && <div className="text-center my-1 py-1 dark:text-white">No more data</div>}
{infiniteScroll.loading && (
<div className="text-center my-1 py-1 dark:text-white animate-pulse">Loading...</div>
)}
{infiniteScroll.isLoadDone && <div className="text-center my-1 py-1 dark:text-white/60">No more data</div>}
</div>
<Button onClick={reset}>Reset List</Button>
</Card>
)
}
85 changes: 55 additions & 30 deletions packages/react-use/src/use-infinite-scroll/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -47,24 +48,36 @@ export interface UseInfiniteScrollOptions<R> {
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<void>
/**
* Calculate the current scroll position to determine whether to load more
*/
calculate(): void
/**
* 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
}

/**
Expand All @@ -86,16 +99,35 @@ export function useInfiniteScroll<R = any, T extends HTMLElement = HTMLElement>(

const el = useTargetElement<T>(target)
const previousReturn = useRef<R | undefined>(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

Expand All @@ -107,27 +139,16 @@ export function useInfiniteScroll<R = any, T extends HTMLElement = HTMLElement>(
: 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,
Expand All @@ -139,15 +160,19 @@ export function useInfiniteScroll<R = any, T extends HTMLElement = HTMLElement>(
{ 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,
}
}

0 comments on commit 20d4fd8

Please sign in to comment.