React自定义Hook之useInfiniteScroll无限滚动

前言

当我们编写网页应用时,经常会遇到需要在页面中渲染大量数据的需求。如果你对性能要求不高(偷懒),我们可以一次性将所有数据渲染出来,或者让用户手动点击去加载更多数据,但是这样付出的代价就是用户体验非常不好。

在我的上篇博客《React自定义Hook之useInView可视区检测》中,曾经提到可以基于useInView实现无限滚动的功能,在这篇blog中,我们来结合useInView实现一下这个功能,并封装另一个一个实用的React Hook

简介

本文主要讲述如何实现无限滚动功能(前端页面大数据量的加载优化),并封装一个基本的React Hook。useInfiniteScroll旨在简化实现大数据量无限滚动的过程,Hook内部提供了一种轻松的方式来加载分页数据。通过这个 Hook,可以改善用户体验,避免了用户需要手动点击按钮去翻页的场景,也减少了开发者的工作量(少掉几根头发)。

一、定义Hook内部的核心概念

我们先来定义useInfiniteScroll 内部的核心概念:

入参:
  • dataSource:一次性从后台接口获取的所有数据或者本地定义的静态数据。
  • delay:模拟加载延迟,可以设置一个延迟时间,以更好地模拟实际加载情况。
  • pageSize:每页需要加载的数据数量。
  • fetchData:动态从服务器获取数据的异步函数(分页接口)。
  • dataSourcefetchData必须传入一个,当获取数据的接口为非分页接口时(后台偷懒【旺柴】),可以一次性请求所有数据,传入dataSource
Hook暴露出的回参:
  • data: 当前已加载的数据。
  • loading: 加载状态。
  • hasMore: 是否还有更多数据。
  • loadMore; 加载更多数据的函数。
Hook内部流程:
  1. 检测到用户滚动到页面底部时,调用loadMore 函数。
  2. 当数据加载完毕或正在加载数据,不会触发函数。
  3. 如果传入 dataSource 数据源,数据将从数据源中分页加载。
  4. 如果传入 fetchData 函数,Hook内部将提供pageNumpageSize参数来请求接口获取更多数据。
  5. 更新加载状态、最新数据以及检查是否还有更多数据可供加载。
  6. 用户可以继续滚动并加载下一页数据。

二、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,可以让你的前端应用更加动态、流畅,并为用户提供更好的滚动体验。

相关推荐
小白学习日记39 分钟前
【复习】HTML常用标签<table>
前端·html
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端
北极小狐2 小时前
浏览器事件处理机制:从硬件中断到事件驱动
前端
无咎.lsy2 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec2 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron