前言
当我们编写网页应用时,经常会遇到需要在页面中渲染大量数据的需求。如果你对性能要求不高(偷懒),我们可以一次性将所有数据渲染出来,或者让用户手动点击去加载更多数据,但是这样付出的代价就是用户体验非常不好。
在我的上篇博客《React自定义Hook之useInView可视区检测》中,曾经提到可以基于useInView
实现无限滚动的功能,在这篇blog中,我们来结合useInView
实现一下这个功能,并封装另一个一个实用的React Hook
。
简介
本文主要讲述如何实现无限滚动功能(前端页面大数据量的加载优化),并封装一个基本的React Hook。useInfiniteScroll
旨在简化实现大数据量无限滚动的过程,Hook内部提供了一种轻松的方式来加载分页数据。通过这个 Hook,可以改善用户体验,避免了用户需要手动点击按钮去翻页的场景,也减少了开发者的工作量(少掉几根头发)。
一、定义Hook内部的核心概念
我们先来定义useInfiniteScroll
内部的核心概念:
入参:
dataSource
:一次性从后台接口获取的所有数据或者本地定义的静态数据。delay
:模拟加载延迟,可以设置一个延迟时间,以更好地模拟实际加载情况。pageSize
:每页需要加载的数据数量。fetchData
:动态从服务器获取数据的异步函数(分页接口)。dataSource
和fetchData
必须传入一个,当获取数据的接口为非分页接口
时(后台偷懒【旺柴】),可以一次性请求所有数据,传入dataSource
。
Hook暴露出的回参:
data
: 当前已加载的数据。loading
: 加载状态。hasMore
: 是否还有更多数据。loadMore
; 加载更多数据的函数。
Hook内部流程:
- 检测到用户滚动到页面底部时,调用
loadMore
函数。 - 当数据加载完毕或正在加载数据,不会触发函数。
- 如果传入
dataSource
数据源,数据将从数据源中分页加载。 - 如果传入
fetchData
函数,Hook内部将提供pageNum
和pageSize
参数来请求接口获取更多数据。 - 更新加载状态、最新数据以及检查是否还有更多数据可供加载。
- 用户可以继续滚动并加载下一页数据。
二、useInfiniteScroll
具体代码实现
js版本
js
import { useState } from 'react';
export default function useInfiniteScroll({
dataSource,
delay = 100,
pageSize = 10,
fetchData,
}) {
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [data, setData] = useState([]);
async function loadMore() {
// 如果数据源为空且没有提供加载数据的函数,直接返回
if (!dataSource?.length && !fetchData) return;
// 如果没有更多数据或正在加载,直接返回
if (!hasMore || loading) return;
setLoading(true);
if (dataSource) {
// 从数据源中加载更多数据
await new Promise((resolve) => {
setTimeout(() => {
resolve(dataSource?.slice(data.length, data.length + pageSize));
}, delay);
}).then((list) => {
// 更新是否还有更多数据和已加载的数据
setHasMore(data?.length + list?.length < dataSource?.length);
setData((value) => value?.concat(list));
});
} else {
// 通过提供的 fetchData 函数加载更多数据
await fetchData?.({
pageNum: data?.length
? Math.ceil(data?.length / pageSize) + 1
: 1,
pageSize,
}).then(({ list = [], total = 0 }) => {
// 更新是否还有更多数据和已加载的数据
setHasMore(data?.length + list?.length < total && list?.length > 0);
setData((value) => value?.concat(list));
});
}
setLoading(false);
}
return {
data, // 当前已加载的数据
loading, // 加载状态
hasMore, // 是否还有更多数据
loadMore, // 加载更多数据的函数
};
}
ts版本
ts
import { useState } from 'react';
interface UseInfiniteScrollProps<T> {
dataSource?: T[]; // 数据源
delay?: number; // 延迟加载时间
pageSize?: number; // 每页数据项数量
fetchData?: (params: { pageSize: number; pageNum: number; }) => Promise<{
total?: number; // 总数据数
list?: T[]; // 当前页的数据列表
}>;
}
/**
* 无限滚动 Hook
* @param param
* @returns
*/
export default function useInfiniteScroll<T = any>({
dataSource,
delay = 100,
pageSize = 10,
fetchData,
}: UseInfiniteScrollProps<T>) {
const [loading, setLoading] = useState<boolean>(false); // 加载状态
const [hasMore, setHasMore] = useState<boolean>(true); // 是否还有更多数据
const [data, setData] = useState<T[]>([]); // 当前已加载的数据列表
async function loadMore() {
// 如果数据源为空且没有提供加载数据的函数,直接返回
if (!dataSource?.length && !fetchData) return;
// 如果没有更多数据或正在加载,直接返回
if (!hasMore || loading) return;
setLoading(true);
if (dataSource) {
// 从数据源中加载更多数据
await new Promise<T[]>((resolve) => {
setTimeout(() => {
resolve(dataSource?.slice(data.length, data.length + pageSize));
}, delay);
}).then((list) => {
setHasMore(data?.length + list?.length < dataSource?.length);
setData((value) => value?.concat(list as T[]));
});
} else {
// 通过 fetchData 函数加载更多数据
await fetchData?.({
pageNum: data?.length
? Math.ceil(data?.length / pageSize) + 1
: 1,
pageSize,
}).then(({ list = [], total = 0 }) => {
setHasMore(data?.length + list?.length < total && list?.length > 0);
setData((value) => value?.concat(list));
});
}
setLoading(false);
}
return {
data, // 当前已加载的数据
loading, // 加载状态
hasMore, // 是否还有更多数据
loadMore, // 加载更多数据的函数
};
}
三、使用useInView
封装加载器组件、结合useInfiniteScroll
实现无限滚动加载
在Hook内部流程介绍中,首先第一步,我们需要检测用户是否滚动到页面底部,可以先看看我的上篇blog《React自定义Hook之useInView可视区检测》,我们使用 useInView
Hook来封装一个简易的加载器组件,检测到加载器在页面中时,去调用加载更多的函数。
InfiniteScrollTrigger
组件
js
import { useEffect } from 'react';
import useInView from './useInView';
export default function InfiniteScrollTrigger({ hasMore, loadMore }) {
const [targetRef, inView] = useInView()
useEffect(() => {
if (inView && hasMore) loadMore?.()
}, [hasMore, inView, loadMore])
return <div ref={targetRef}>{hasMore ? '加载中...' : '没有更多了~'}</div>
}
传入数据源分页:我们可以一次性获取所有数据,useInfiniteScroll
会自动帮你分页
js
function Page(){
// mock接口获取的数据
const dataSource = new Array(10000).fill(1).map((item,index) => index)
const { data, hasMore, loadMore } = useInfiniteScroll({
dataSource,// 所有数据源
pageSize: 10, // 一次性加载10条
delay: 100, // 100ms延时
})
return(<div>
{
data?.map((item) => {
return <span>{item}</span>
})
}
<InfiniteScrollTrigger loadMore={loadMore} hasMore={hasMore} />
</div>)
}
动态请求分页:也可以通过分页接口来动态获取每页数据
js
function Page(){
const { data, hasMore, loadMore } = useInfiniteScroll({
pageSize: 10, // 一次性加载10条
delay: 100, // 100ms延时
async fetchData({ pageSize, pageNum }) {
// 请求分页接口
const { list = [], total = 0 } = await getData({
pageSize, // 一次性加载10条
pageNum, // 当前页码
...{}, // 自定义参数
})
return { list, total }
},
})
return(<div>
{
data?.map((item) => {
return <span>{item}</span>
})
}
<InfiniteScrollTrigger loadMore={loadMore} hasMore={hasMore} />
</div>)
}
四、总结
useInfiniteScroll
是一个强大的Hook,可以帮助你轻松实现无限滚动功能,提高用户体验并减少用户的交互需求。它通过动态请求分页和数据源分页的方式,适用于多种使用场景,如:
- 需要优化渲染性能的长列表页面,例如社交媒体的新闻源、聊天记录等。
- 需要减少用户的交互,提高用户体验,避免分页或手动点击"加载更多"按钮。
- 在页面中需要加载大量数据,但又不希望一次性加载所有数据。
使用这个自定义 Hook,可以让你的前端应用更加动态、流畅,并为用户提供更好的滚动体验。