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}/>)
                                  }
                              }}
                    />
相关推荐
yilan_n2 小时前
【UniApp实战】手撸面包屑导航与路由管理 (拒绝页面闪烁)
前端·javascript·vue.js·uni-app·gitcode
lang201509282 小时前
深入解析Sentinel熔断机制
java·前端·sentinel
Highcharts.js2 小时前
官方文档|Vue 集成 Highcharts Dashboards
前端·javascript·vue.js·技术文档·highcharts·看板·dashboards
Misha韩2 小时前
vue3+vite模块联邦 ----子应用中页面如何跳转传参
前端·javascript·vue.js·微前端·模块联邦
乖女子@@@2 小时前
01ReactNative-环境搭建
javascript·react native·react.js
开发者小天2 小时前
react中的使用useReducer和Context实现todolist
前端·javascript·react.js
Youyzq2 小时前
react-inlinesvg如何动态的修改颜色SVG
前端·react.js·前端框架
wniuniu_2 小时前
rbd创建特定的用户
前端·chrome
老前端的功夫2 小时前
Webpack打包机制与Babel转译原理深度解析
前端·javascript·vue.js·webpack·架构·前端框架·node.js