加载更多、加载所有 组件的逻辑封装的3种实现方式以及对比

1、使用model + 递归
2、自定义hook + async + 递归
3、自定义hook + useSWRInfinite + 监听

情景描述:
列表接口返回的数据格式是:

{
has_next:boolean,
items:[],
next:{hash_table,hash_index}//下一次请求需要这个参数。作为游标传入。
}

现在需要实现加载更多、加载所有的功能。
除此之外需要考虑2种情况:修改某条数据的功能、等待请求之后全部完成后再做后续的操作。

1、使用model (dva) + 递归

原理:

  • 使用model的生成器函数的,yeild,来实现 异步的问题的等待过程。
  • 根据has_next来递归调用effect的方法。来实现加载所有。

缺点:

  • 用到model ,导致整个项目比较重
  • 等待请求之后全部完成后再做后续的操作:要用 callback实现

优点:

  • 方便数据管理
  • 能直接用loading,不用自己维护
  • 方便修改数据

//加载更多
	*queryCustomerListAll({ payload, callback }, { call, select, put }) {
      const { items = [], has_next, next } = yield call(getCustomerInfoList, payload);//获取本次的数据
      const list = yield select((state: ConnectState) => state.batchTools.customer.list);//去拿旧数据
      const newItems = [...list, ...items];
      yield put({
        type: 'saveData',
        payload: {
          customers: { items : newItems, has_next, next },
        },
      });
   }

//加载所有
    *queryCustomerListAll({ payload, callback }, { call, select, put }) {
      const { items = [], has_next, next } = yield call(getCustomerInfoList, payload);//获取本次的数据
      const list = yield select((state: ConnectState) => state.batchTools.customer.list);//去拿旧数据
      const newItems = [...list, ...items];
      yield put({
        type: 'saveData',
        payload: {
          customers: { items : newItems, has_next, next },
        },
      });
      //递归调用 !!! 
      if (has_next) {
        const params = {...payload,...next,};
        yield put({ type: 'queryCustomerList', payload: params, callback });
      } else {
        callback(newItems);
      }
    },
2、自定义hook + async +递归

原理:

  • 使用async await ,来实现 接口调用的等待。
  • 根据has_next来递归调用fetchActionAll的方法。来实现加载所有。

缺点:

  • 所有的都要动手写。不如方法1 来的快。但是封装好后也好用。
  • 要自己加loading 控制。
  • 等待请求之后全部完成后再做后续的操作:要用监听实现,监听loading的变化。或者使用callback放到fetchActionAll里,同上。

优点:

  • 不需要重复的写model,
  • 封装的比较全。包括修改、清空等功能。
  • 方便修改数据

封装心得:

  • 最好,每个功能的时候 对应着 往外抛出的一个方法。不要用setState(xx)来控制子hOOk里的state。然后用监听再去处理。这样很容易导致混乱。最后不知道是哪一步导致的变化。
  • 对于入参尤其是可配置的下拉框之类,(比如页码的可选项和默认项)最好加个defaultOption。做成可配置的。
  • 这个只是自定义hook的封装。和组件的封装抽离开了。以后需要的话可以考虑逻辑和静态页面分开封装控制。
  • 命名、抛出的参数、等可参考antd库借鉴经验。
import { getPerPageSize, hashCode, setPerPageSize } from '@/utils/utils';
import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import { sortBy } from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'umi';
import type {
  PaginationListSchema,
  PaginationListUpdateResponseSchema,
  PaginationListUpdateSchema,
} from '../../schema';

type searchPropsType<T> = {
  params?: DefaultParamsType<T> | Record<string, never>;
  isReload?: boolean;
};

type usePaginatedResult<T, P, S> = {
  paginationLoading: { loading?: boolean; loadingAll?: boolean; loadingUpdate?: boolean };
  $Pagination: {
    onSearch: (searchProps: searchPropsType<T>) => void;
    onSearchAll: () => void;
    clearData: () => void;
    updateData: (params: S, formatMessageId?: string) => void;
    onPageSizeChange: (value: string) => void;
  };
  data: PaginationListSchema<P>;
  paginationOptions: {
    pageSize: string;
    pageSizeOptions: string[];
  };
};
type usePaginatedProps<T, P, S> = {
  action: (pageOptions: T) => Promise<PaginationListSchema<P>>;
  updateAction?: (payload: S) => Promise<PaginationListUpdateResponseSchema>;
  defaultParams?: DefaultParamsType<T>; // 是页面初始化的参数 是不带next和per_page
  defaultPageSizeOptions?: string[];
  defaultPageSize?: string;
  isDefaultSearch?: boolean;
};
type DefaultParamsType<T> = Omit<T, 'per_page'>;

// T:action的接口入参。P:action的接口返回 , K :更新的入参
export default function usePaginationList<T, P, S = PaginationListUpdateSchema>(
  props: usePaginatedProps<T, P, S>,
): usePaginatedResult<T, P, S> {
  const {
    action,
    updateAction,
    defaultParams,
    isDefaultSearch = false,
    defaultPageSize,
    defaultPageSizeOptions = ['10', '50', '100'],
  } = props;

  const initData = { items: [], has_next: false, next: {} };
  const [data, setData] = useState<PaginationListSchema<P>>(initData);
  const [loading, setLoading] = useState<boolean | undefined>();
  const [loadingAll, setLoadingAll] = useState<boolean | undefined>();
  const [loadingUpdate, setUpdateLoading] = useState<boolean>(false);
  const [pageSizeOptions, setPageSizeOptions] = useState(defaultPageSizeOptions);
  const perPageSize = getPerPageSize();
  const [pageSize, setPageSize] = useState<string>(
    defaultPageSize || perPageSize || defaultPageSizeOptions[0],
  );
  const actionParams = useRef<DefaultParamsType<T> | Record<string, never>>(defaultParams || {});
  const { formatMessage } = useIntl();

  // 如果isReload true,代表是更新了查询参数,不需要继承历史items
  const fetchAction = async (searchProps: searchPropsType<T>) => {
    setLoading(true);

    const { isReload, params } = searchProps;
    const preItems = isReload ? [] : data.items;

    const { has_next, next, items: newItems = [] } = await action({
      per_page: pageSize,
      ...params,
    } as T);

    const newData = { has_next, next, items: [...preItems, ...newItems] };
    setData(newData);
    setLoading(false);
  };

  const fetchActionAll = async ({
    next: preNext = {},
    items: oldItems,
  }: PaginationListSchema<P>) => {
    const { has_next, next, items: newItems } = await action({
      ...actionParams.current,
      ...preNext,
    } as T);

    const newData = { has_next, next, items: [...oldItems, ...newItems] };
    if (has_next) {
      fetchActionAll(newData);
    } else {
      setData(newData);
      setLoadingAll(false);
    }
  };

  // 加载更多(isReload:false) + 重新加载(isReload:true)
  const onSearch = useMemoizedFn((searchProps: searchPropsType<T>) => {
    const { params = {} } = searchProps;
    fetchAction({ ...searchProps, params });
    if (hashCode(params) !== hashCode(actionParams.current)) actionParams.current = params;
  });

  // 加载所有
  const onSearchAll = useMemoizedFn(() => {
    setLoadingAll(true);
    fetchActionAll(data);
  });

  // 分页变化
  const onPageSizeChange = useMemoizedFn((size: string) => {
    setPerPageSize(size);
    setPageSize(size);
  });

  // 清空数据
  const clearData = useMemoizedFn(() => {
    setData(initData);
  });

  // 更新数据
  const updateData = useMemoizedFn(async (payload: S, formatMessageId?: string) => {
    if (updateAction) {
      setUpdateLoading(true);
      // 请求
      const { items } = data;
      const response = await updateAction(payload);
      if (response?.success) {
        const { data: newData, value, key } = payload as PaginationListUpdateSchema;
        const index = items.findIndex((o: P) => {
          return o[key] === value;
        });
        const newItems = [...items];
        newItems[index] = { ...items[index], ...newData };
        setData({ ...data, items: newItems }); // 更新数据
      }
      // 提示
      if (response?.success && formatMessageId) {
        message.success(formatMessage({ id: `${formatMessageId}.ok` }));
      } else if (formatMessageId) {
        message.error(formatMessage({ id: `${formatMessageId}.error` }));
      }
      setUpdateLoading(false);
    }
  });

  useEffect(() => {
    // 存储到本地
    setPerPageSize(pageSize);

    // 防止默认的pagesize不在下拉选择里
    let newArr: string[] = [...pageSizeOptions];
    const index = pageSizeOptions.findIndex((value) => {
      return value === pageSize;
    });
    if (index === -1) {
      newArr = [...newArr, pageSize];
    }
    // 排序
    newArr = sortBy(newArr, (value) => Number(value));
    setPageSizeOptions(newArr);

    // 默认加载
    if (isDefaultSearch) {
      onSearch({ params: defaultParams || {}, isReload: true });
    }

    return () => {
      clearData();
    };
  }, []);

  return {
    paginationLoading: { loading, loadingAll, loadingUpdate },
    data,
    $Pagination: { onSearch, onSearchAll, clearData, updateData, onPageSizeChange },
    paginationOptions: {
      pageSize,
      pageSizeOptions,
    },
  };
}
3、自定义hook + useSWRInfinite + 监听

原理:

  • 使用useSWRInfinite ,来实现 接口调用的等待。能自动返回data和loading。
  • getKey方法是用来生成本次调用的参数的。
  • setIsLoadAll是点击了加载全部时调用。setIsLoadAll(true)
  • 自定义hook 监听了data最新里的has_next、以及isLoadAll来表明此hook进入到加载全部的状态,识别是否需要调用setSize()的方法。来实现加载所有。。

这个方法和上2个不同的地方在于我们没法直接再自己写的方法里通过await,等待接口拿到数据。所以这里只能用 监听最新的data,不断调用setSize。无法用递归。

缺点:

  • useSWRInfinite高度封装,用起来需要学习成本,而且用法我感觉很奇怪。
  • 数据不方便处理。需要额外自定义data。useSWRInfinite抛出的原始data是二维数组。
  • 等待请求之后全部完成后再做后续的操作:要用监听实现,监听loading的变化

优点:

  • 可以对接口数据做缓存(最大的优点)
  • 轻便
  • 有loading。
import { useEffect, useState } from 'react';
import useSWRInfinite from 'swr/infinite';
import { getCustomerInfoList } from './service';
import type { QueryParams, QueryResponse } from './type';

export const useCustomerList = (params: QueryParams, revalidateOnMount: boolean = false) => {
  const [isLoadAll, setIsLoadAll] = useState<boolean>(false);
  const { data, size, setSize, mutate, isValidating } = useSWRInfinite(
    (pageIndex: number, previousPageData: QueryResponse) => {
      // 首页,没有 `previousPageData`
      if (pageIndex === 0) {
        return params;
      }
      // 加载更多
      if (previousPageData && previousPageData.next) {
        return { ...params, ...previousPageData.next };
      }

      // 最后一页
      return null;
    },
    getCustomerInfoList,
    {
      shouldRetryOnError: false,
      revalidateIfStale: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      revalidateOnMount, // 是否初始化加载
      revalidateFirstPage: false,
    },
  );
  const items = data?.flatMap((value) => value.items || []) || [];
  const has_next = data && data[data?.length - 1].has_next;

  // 加载全部
  useEffect(() => {
    if (isLoadAll && has_next) {
      setSize(size + 1);
    }
  }, [isLoadAll, data]);

  return { items, has_next, isLoadingList: isValidating, size, setSize, mutate, setIsLoadAll };
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值