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}/>)
                                  }
                              }}
                    />
相关推荐
GIS之路33 分钟前
ArcGIS Pro 中的 Notebooks 入门
前端
IT_陈寒2 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端
gxp1232 小时前
初学React:请求数据参数未更新 && 数据异步状态更新问题
react.js
Kagol3 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉3 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau3 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生3 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼3 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范
明君879973 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter
leolee184 小时前
Redux Toolkit 实战使用指南
前端·react.js·redux