极致体验的 React 无限滚动组件封装

极致体验的 React 无限滚动组件封装

本文将带你从零打造一个优雅、可复用、国际化友好的 React 无限滚动(Infinite Scroll)组件


1. 背景与需求

在现代 Web 应用中,长列表的高性能渲染流畅的用户体验是不可或缺的。传统的"分页"已无法满足用户对"无缝浏览"的期待。无限滚动(Infinite Scroll)成为了更自然的选择。

但市面上的无限滚动方案往往存在如下痛点:

  • 代码侵入性强,难以复用
  • 交互细节粗糙,缺乏美感
  • 数据同步与状态管理混乱

本项目基于 React ,封装了一个极致体验的 InfiniteScroll 组件,并在实际业务(如告警历史列表)演示使用。


2. 组件设计理念

✨ 设计目标

  • 极简 API:只需传递初始数据、加载函数、渲染函数即可
  • 自动国际化:内置多语言提示,支持自定义
  • 优雅交互:加载动画、无数据提示、分割线美学
  • 数据同步:外部数据变化自动刷新内部状态
  • 类型安全:泛型支持,适配任意数据结构

🧩 组件核心代码

// components/infinite-scroll.tsx.tsx
"use client";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";

interface InfiniteScrollProps<T> {
  initialItems: T[];
  loadMore: (page: number) => Promise<T[]>;
  renderItem: (item: T, index: number) => React.ReactNode;
  pageSize?: number;
  hasMore?: boolean;
  loadingText?: string;
  endMessage?: string;
  className?: string;
  noData?: () => React.ReactNode;
}

export function InfiniteScroll<T>({
  initialItems,
  loadMore,
  renderItem,
  pageSize = 10,
  hasMore: initialHasMore = true,
  loadingText,
  endMessage,
  noData,
  className = "",
}: InfiniteScrollProps<T>) {
  const t = useTranslations("InfiniteScroll");
  const [items, setItems] = useState<T[]>(initialItems);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(initialHasMore);
  const loaderRef = useRef<HTMLDivElement>(null);

  // 外部数据变化时自动同步
  useEffect(() => {
    setItems(initialItems);
    setPage(1);
    setHasMore(initialHasMore);
  }, [initialItems, initialHasMore]);

  // 加载更多数据
  const fetchMoreData = async () => {
    if (loading || !hasMore) return;
    setLoading(true);
    try {
      const newItems = await loadMore(page);
      if (newItems.length === 0 || newItems.length < pageSize) setHasMore(false);
      setItems((prev) => [...prev, ...newItems]);
      setPage((prev) => prev + 1);
    } finally {
      setLoading(false);
    }
  };

  // Intersection Observer 监听滚动
  useEffect(() => {
    const currentLoaderRef = loaderRef.current;
    if (!currentLoaderRef) return;
    const observer = new IntersectionObserver(
      (entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasMore && !loading) fetchMoreData();
      },
      { threshold: 0.1 }
    );
    observer.observe(currentLoaderRef);
    return () => {
      if (currentLoaderRef) observer.unobserve(currentLoaderRef);
    };
  }, [hasMore, loading]);

  return (
    <div className={`w-full ${className}`}>
      <div className="w-full">
        {items.length > 0 ? (
          items.map((item, index) => (
            <div key={index} className="w-full">
              {renderItem(item, index)}
            </div>
          ))
        ) : (
          <div className="w-full">{noData ? noData() : t("noData")}</div>
        )}
      </div>
      {(loading || hasMore) && (
        <div
          ref={loaderRef}
          className="flex w-full items-center justify-center py-4 text-sm text-muted-foreground"
        >
          {loading ? (
            <div className="flex items-center gap-2">
              <Loader2 className="h-4 w-4 animate-spin" />
              <span>{loadingText || t("loadingText")}</span>
            </div>
          ) : hasMore ? (
            <div className="h-8" />
          ) : (
            <span>{endMessage || t("endMessage")}</span>
          )}
        </div>
      )}
    </div>
  );
}

3. 业务场景应用示例

以告警历史列表为例,配合 InfiniteScroll 组件实现无限滚动:

// src/app/[locale]/history/page.tsx
<InfiniteScroll
  className="tg-theme-card rounded-md overflow-hidden"
  initialItems={histories}
  loadMore={loadMore}
  hasMore={hasMore}
  pageSize={PAGE_SIZE}
  renderItem={(item, index) => (
    <HistoryList items={[item]} filter={filter} />
  )}
/>
  • initialItems:首次加载的数据
  • loadMore:分页加载函数,返回 Promise
  • renderItem:自定义每一项的渲染

4. 细节美学与体验优化

  • 加载动画:加载中有旋转动画,用户感知流畅
  • 无数据提示:优雅的空状态,支持自定义
  • 数据同步:外部数据变化自动刷新,无需手动刷新页面
  • 移动端友好:交互区域大,动画反馈自然
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值