React 分页轻量化封装

相比于https://github.com/vercel/swr 我们常用自己的封装的api进行请求,不需要绑定具体的网络框架

TypeScript 复制代码
    ///滚动加载demo
    const [pageNum, setPageNum] = useState(1);
    const [notificationList, setNotificationList] = useState<NotificationItemDTO[]>([]);

    /// 真实请求数据
    const asyncResult = useAsync(async () => {
        const value = await notificationService
            .getNotificationList(pageNum, 50)
            .bindErrorNotice()

        setNotificationList(prev => pageNum === 1 ? value.list : [...prev, ...value.list]);
        return value;
    }, [pageNum]); // pageNum 变化时重新执行


    /// 处理加载更多
    const handleLoadMore = useHandleLoadMore(asyncResult, setPageNum)

实现:用react-async-hook进行二次封装

TypeScript 复制代码
import {useCallback, useEffect, useRef} from "react";
import {ApiPageDataFieldDTO} from "@/lib/models/ApiResponse";
import {hasNextPage} from "@/lib/models/PaginationDTO";

/**
 * 返回一个稳定的回调(empty deps)。
 * 适合不想因 asyncResult 变化频繁重建回调的场景。
 *
 * 用法:
 * const handleLoadMore = useStableHandleLoadMore(asyncResult, setPageNum);
 * // 自定义是否有下一页的判定逻辑
 * // const handleLoadMore = useHandleLoadMoreCustom<TemplatePaginationResponse, UseAsyncReturn<TemplatePaginationResponse>>(asyncResult, setPageNum, (data) => hasNextPage(data?.pagination))
 *
 * 在对应的组件上使用
 *     endReached={handleLoadMore}
 *     atBottomStateChange={(atBottom) => {
 *       if (atBottom) {
 *          handleLoadMore()
 *       }
 *    }}
 */
export function useHandleLoadMore<TData extends ApiPageDataFieldDTO<any>, TAsyncResult extends {
    loading: boolean;
    error: unknown;
    execute: () => Promise<TData>;
    result?: TData
}>(
    asyncResult: TAsyncResult,
    setPageNum: (updater: (p: number) => number) => void,
) {
    return useHandleLoadMoreCustom<ApiPageDataFieldDTO<any>, TAsyncResult>(
        asyncResult,
        setPageNum,
        (data) => hasNextPage(data?.pagination)
    );
}


/**
 * 可自定义 hasNextPage 判定的版本(名称:useHandleLoadMoreCustom)
 * - hasNextPageFn 接受完整的 TData(可能为 undefined),返回 boolean
 * - 返回稳定回调,内部通过 ref 读取最新 asyncResult 与 hasNextPageFn
 */
export function useHandleLoadMoreCustom<TData, TAsyncResult extends {
    loading: boolean;
    error: unknown;
    execute?: () => Promise<TData>;
    result?: TData;
}>(
    asyncResult: TAsyncResult,
    setPageNum: (updater: (p: number) => number) => void,
    hasNextPage: (data: TData | undefined) => boolean,
): () => void {

    const ref = useRef<TAsyncResult>(asyncResult);
    useEffect(() => {
        ref.current = asyncResult;
    }, [asyncResult]);


    // 保证读取到最新的判断函数,同时回调保持稳定(不随函数重建)
    const hasNextRef = useRef<(data: TData | undefined) => boolean>(
        (d) => hasNextPage((d as any)?.pagination)
    );
    useEffect(() => {
        hasNextRef.current = hasNextPage;
    }, [hasNextPage]);

    return useCallback(() => {
        const cur = ref.current;
        if (!cur) return;
        if (cur.loading) return;
        if (cur.error) {
            cur.execute?.();
            return;
        }

        // 使用最新的 hasNext 判定
        if (!hasNextRef.current(cur.result)) return;

        ///进行下一页
        setPageNum(p => p + 1);
    }, [setPageNum]);
}

其他类

TypeScript 复制代码
// API 响应接口
import {PaginationDTO} from "@/lib/models/PaginationDTO";

/**
 * 根组件API响应格式
 */
export interface ApiResponse<T> {
    code: number
    message: string
    data: T
}

/**
 * 分页数据字段接口
 */
export interface ApiPageDataFieldDTO<T> {
    list: T[]
    pagination: PaginationDTO
}

export const ApiResponseUtils = {
    /**
     * 获取BODY中的data field字段,如果不是成功状态将抛出异常
     * @param body
     */
    getDataFieldOrThrow<T>(body: ApiResponse<T>) {
        const code = body.code
        if (code !== 0 && code !== 200) {
            ///格式不要动
            throw new Error(`API Error: (${code})${body.message}`)
        }
        return body.data
    }
}

ui组件进行绑定

TypeScript 复制代码
<Virtuoso className="w-full flex-1 min-h-[50vh] max-h-[70vh]"
                              data={notificationList}
                              itemContent={(index, notification) => {
                                  return (<div key={notification.id}
                                               onClick={() => {
                                                   clearNotification(notification.id)
                                               }}>
                                      <NotificationCard
                                          key={notification.id}
                                          notification={notification}
                                          isFirst={index === 0}
                                      />
                                  </div>)
                              }}
                              endReached={handleLoadMore}
                              atBottomStateChange={(atBottom) => {
                                  if (atBottom) {
                                      handleLoadMore()
                                  }
                              }}
                              overscan={200}
                              components={{
                                  Footer: () => {
                                      return (<Refresh.Footer hasMore={hasNextPage(asyncResult.result?.pagination)}
                                                              isLoading={asyncResult.loading}
                                                              error={asyncResult.error}/>)
                                  }
                              }}
                    />
相关推荐
清山博客8 小时前
OpenCV 人脸识别和比对工具
前端·webpack·node.js
要加油哦~9 小时前
AI | 实践教程 - ScreenCoder | 多agents前端代码生成
前端·javascript·人工智能
程序员Sunday9 小时前
说点不一样的。GPT-5.3 与 Claude Opus 4.6 同时炸场,前端变天了?
前端·gpt·状态模式
yq1982043011569 小时前
静思书屋:基于Java Web技术栈构建高性能图书信息平台实践
java·开发语言·前端
aPurpleBerry9 小时前
monorepo (Monolithic Repository) pnpm rush
前端
青茶3609 小时前
php怎么实现订单接口状态轮询请求
前端·javascript·php
鹏北海9 小时前
micro-app 微前端项目部署指南
前端·nginx·微服务
发现一只大呆瓜9 小时前
虚拟列表:从定高到动态高度的 Vue 3 & React 满分实现
前端·vue.js·react.js
css趣多多9 小时前
add组件增删改的表单处理
java·服务器·前端
证榜样呀9 小时前
2026 大专计算机专业必考证书推荐什么
大数据·前端