加载更多、加载所有 组件的逻辑封装的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_pagedefaultPageSizeOptions?: 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,代表是更新了查询参数,不需要继承历史itemsconst 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 };
};
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
