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 };
};