{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "useInfiniteScroll", "title": "useInfiniteScroll", "description": "useInfiniteScroll hook", "registryDependencies": [ "https://shadcn-ahooks.vercel.app/r/useMemoizedFn.json", "https://shadcn-ahooks.vercel.app/r/useEventListener.json", "https://shadcn-ahooks.vercel.app/r/useRequest.json", "https://shadcn-ahooks.vercel.app/r/useUpdateEffect.json", "https://shadcn-ahooks.vercel.app/r/utils.json" ], "files": [ { "path": "packages/hooks/src/useInfiniteScroll/index.tsx", "content": "import { useMemo, useRef, useState } from 'react';\nimport useEventListener from '../useEventListener';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useRequest from '../useRequest';\nimport useUpdateEffect from '../useUpdateEffect';\nimport { getTargetElement } from '../utils/domTarget';\nimport { getClientHeight, getScrollHeight, getScrollTop } from '../utils/rect';\nimport type { Data, InfiniteScrollOptions, Service } from './types';\n\nconst useInfiniteScroll = (\n service: Service,\n options: InfiniteScrollOptions = {},\n) => {\n const {\n target,\n isNoMore,\n threshold = 100,\n direction = 'bottom',\n reloadDeps = [],\n manual,\n onBefore,\n onSuccess,\n onError,\n onFinally,\n } = options;\n\n const [finalData, setFinalData] = useState();\n const [loadingMore, setLoadingMore] = useState(false);\n const isScrollToTop = direction === 'top';\n // lastScrollTop is used to determine whether the scroll direction is up or down\n const lastScrollTop = useRef(undefined);\n // scrollBottom is used to record the distance from the bottom of the scroll bar\n const scrollBottom = useRef(0);\n\n const noMore = useMemo(() => {\n if (!isNoMore) {\n return false;\n }\n return isNoMore(finalData);\n }, [finalData]);\n\n const { loading, error, run, runAsync, cancel } = useRequest(\n async (lastData?: TData) => {\n const currentData = await service(lastData);\n return { currentData, lastData };\n },\n {\n manual,\n onFinally: (_, d, e) => {\n setLoadingMore(false);\n onFinally?.(d?.currentData, e);\n },\n onBefore: () => onBefore?.(),\n onSuccess: (d) => {\n if (!d.lastData) {\n setFinalData({\n ...d.currentData,\n list: [...(d.currentData.list ?? [])],\n });\n } else {\n setFinalData({\n ...d.currentData,\n list: isScrollToTop\n ? [...d.currentData.list, ...(d.lastData.list ?? [])]\n : [...(d.lastData.list ?? []), ...d.currentData.list],\n });\n }\n\n setTimeout(() => {\n // use requestAnimationFrame to ensure the scroll position is updated (To ensure compatibility react 19)\n requestAnimationFrame(() => {\n if (isScrollToTop) {\n let el = getTargetElement(target);\n el = el === document ? document.documentElement : el;\n if (el) {\n const scrollHeight = getScrollHeight(el);\n (el as Element).scrollTo(0, scrollHeight - scrollBottom.current);\n }\n } else {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n scrollMethod();\n }\n });\n });\n\n onSuccess?.(d.currentData);\n },\n onError: (e) => onError?.(e),\n },\n );\n\n const loadMore = useMemoizedFn(() => {\n if (noMore) {\n return;\n }\n setLoadingMore(true);\n run(finalData);\n });\n\n const runAsyncForCurrent = async (data?: TData) => {\n const res = await runAsync(data);\n return res.currentData;\n };\n\n const loadMoreAsync = useMemoizedFn(() => {\n if (noMore) {\n return Promise.reject();\n }\n setLoadingMore(true);\n return runAsyncForCurrent(finalData);\n });\n\n const reload = () => {\n setLoadingMore(false);\n return run();\n };\n\n const reloadAsync = () => {\n setLoadingMore(false);\n return runAsyncForCurrent();\n };\n\n const scrollMethod = () => {\n const el = getTargetElement(target);\n if (!el) {\n return;\n }\n\n const targetEl = el === document ? document.documentElement : el;\n const scrollTop = getScrollTop(targetEl);\n const scrollHeight = getScrollHeight(targetEl);\n const clientHeight = getClientHeight(targetEl);\n\n if (isScrollToTop) {\n if (\n lastScrollTop.current !== undefined &&\n lastScrollTop.current > scrollTop &&\n scrollTop <= threshold\n ) {\n loadMore();\n }\n lastScrollTop.current = scrollTop;\n scrollBottom.current = scrollHeight - scrollTop;\n } else if (scrollHeight - scrollTop <= clientHeight + threshold) {\n loadMore();\n }\n };\n\n useEventListener(\n 'scroll',\n () => {\n if (loading || loadingMore) {\n return;\n }\n scrollMethod();\n },\n { target },\n );\n\n useUpdateEffect(() => {\n run();\n }, [...reloadDeps]);\n\n return {\n data: finalData,\n loading: !loadingMore && loading,\n error,\n loadingMore,\n noMore,\n\n loadMore,\n loadMoreAsync,\n reload: useMemoizedFn(reload),\n reloadAsync: useMemoizedFn(reloadAsync),\n mutate: setFinalData,\n cancel,\n };\n};\n\nexport default useInfiniteScroll;\n", "type": "registry:file", "target": "src/hooks/ahooks/useInfiniteScroll/index.tsx" }, { "path": "packages/hooks/src/useInfiniteScroll/types.ts", "content": "import type { DependencyList } from 'react';\nimport type { BasicTarget } from '../utils/domTarget';\n\nexport type Data = { list: any[]; [key: string]: any };\n\nexport type Service = (currentData?: TData) => Promise;\n\nexport interface InfiniteScrollResult {\n data: TData;\n loading: boolean;\n loadingMore: boolean;\n error?: Error;\n noMore: boolean;\n\n loadMore: () => void;\n loadMoreAsync: () => Promise;\n reload: () => void;\n reloadAsync: () => Promise;\n cancel: () => void;\n mutate: (data?: TData) => void;\n}\n\nexport interface InfiniteScrollOptions {\n target?: BasicTarget;\n isNoMore?: (data?: TData) => boolean;\n threshold?: number;\n direction?: 'bottom' | 'top';\n\n manual?: boolean;\n reloadDeps?: DependencyList;\n\n onBefore?: () => void;\n onSuccess?: (data: TData) => void;\n onError?: (e: Error) => void;\n onFinally?: (data?: TData, e?: Error) => void;\n}\n", "type": "registry:file", "target": "src/hooks/ahooks/useInfiniteScroll/types.ts" } ], "type": "registry:hook" }