一、useInfiniteScroll的介绍和用法
useInfiniteScroll 封装了常见的无限滚动逻辑。
const { data, loading, loadingMore, loadMore } = useInfiniteScroll(service);
useInfiniteScroll 的第一个参数 service 是一个异步函数,对这个函数的入参和出参有如下约定:
service返回的数据必须包含list数组,类型为{ list: any[], ...rest }service的入参为整合后的最新data
假如第一次请求返回数据为 { list: [1, 2, 3], nextId: 4 }, 第二次返回的数据为 { list: [4, 5, 6], nextId: 7 }, 则我们会自动合并 list,整合后的的 data 为 { list: [1, 2, 3, 4, 5, 6], nextId: 7 }。
二、源码解析
1、参数和配置项
useInfiniteScroll的第一个参数是请求服务,第二个参数是配置项。
const useInfiniteScroll = <TData extends Data>(
service: Service<TData>,
options: InfiniteScrollOptions<TData> = {},
) { ... }
我们来看看配置项:
const {
target, // 滚动加载时的容器
isNoMore, // 是否还有更多数据
threshold = 100, // 滚动的边界
reloadDeps = [], // 重置请求时的依赖项
manual,
onBefore,
onSuccess,
onError,
onFinally,
} = options;
2、请求的逻辑
判断是否没有更多数据的方法是外面传进来的:
// 判断是否没有更多了
const noMore = useMemo(() => {
if (!isNoMore) return false;
return isNoMore(finalData);
}, [finalData]);
接下来我们看看执行请求的逻辑:
const { loading, run, runAsync, cancel } = useRequest(
// 请求数据,并且和旧的数据合并
async (lastData?: TData) => {
const currentData = await service(lastData);
if (!lastData) {
setFinalData(currentData);
} else {
setFinalData({
...currentData,
// @ts-ignore
list: [...lastData.list, ...currentData.list],
});
}
return currentData;
},
// 执行`useInfiniteScroll`的各个钩子,设置LoadingMore状态
{
manual,
onFinally: (_, d, e) => {
setLoadingMore(false);
onFinally?.(d, e);
},
onBefore: () => onBefore?.(),
// 请求成功后,setTimeout等页面渲染完后,如果一页的数据过少,则要触发加载下一页
onSuccess: (d) => {
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
scrollMethod();
});
onSuccess?.(d);
},
onError: (e) => onError?.(e),
},
);
其实这里的代码很简单,调用useRequest:
- 传入
useRequest需要的请求服务,在服务里,执行useInfiniteScroll的service服务获取新数据,再和旧数据进行合并。 - 在
useRequest钩子里执行useInfiniteScroll的钩子,设置loadingMore状态。 - 请求成功后,看看能发触发滚动到底的逻辑(在一页数据撑不满一屏的情况下)。
// 执行加载下一页
const loadMore = () => {
if (noMore) return;
setLoadingMore(true);
run(finalData);
};
// 重新加载的依赖变化时,重新加载
useUpdateEffect(() => {
run();
}, [...reloadDeps]);
3、执行下一页和重新加载
至于执行下一页,其实就是执行run方法,并且把旧的列表数据传进去。
// 不传入旧数据,则是重新加载
const reload = () => run();
如果不传旧数据就是重新加载,因为不会和旧数据进行合并。
4、滚动加载数据
// 判断目标元素是否滚动到底部,然后加载
const scrollMethod = () => {
...
if (scrollHeight - scrollTop <= clientHeight + threshold) {
loadMore();
}
};
// 滚动事件监听,useEventListener封装了移除监听的逻辑
useEventListener(
'scroll',
() => {
if (loading || loadingMore) {
return;
}
scrollMethod();
},
{ target },
);
5、返回参数
return {
data: finalData,
loading: !loadingMore && loading,
loadingMore,
noMore,
// 返回缓存后的方法引用
loadMore: useMemoizedFn(loadMore),
loadMoreAsync: useMemoizedFn(loadMoreAsync),
reload: useMemoizedFn(reload),
reloadAsync: useMemoizedFn(reloadAsync),
mutate: setFinalData,
cancel,
};
返回数据、loading状态,加载下一页,重新加载等方法。值得注意的是,这里返回的方法都是经过缓存引用的方法。