当前位置:网站首页>列表页常见的 hook 封装
列表页常见的 hook 封装
2022-08-01 20:57:00 【GopalFeng】
本文是深入浅出 ahooks 源码系列文章的第十六篇,这个系列的目标主要有以下几点:
- 加深对 React hooks 的理解。
- 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。
- 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。
列表页常见元素
对于一些后台管理系统,典型的列表页包括筛选表单项、Table表格、Pagination分页这三部分。
针对使用 Antd 的系统,在 ahooks 中主要是通过 useAntdTable 和 usePagination 这两个 hook 来封装。
usePagination
usePagination 基于 useRequest 实现,封装了常见的分页逻辑。
首先通过 useRequest 处理请求,service 约定返回的数据结构为 { total: number, list: Item[] }。
其中 useRequest 的 defaultParams 参数第一个参数为 { current: number, pageSize: number }。并根据请求的参数以及返回的 total 值,得出总的页数。
还有 refreshDeps 变化,会重置 current 到第一页「changeCurrent(1)」,并重新发起请求,一般你可以把 pagination 依赖的条件放这里。
const usePagination = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: PaginationOptions<TData, TParams> = {},
) => {
const { defaultPageSize = 10, ...rest } = options;
// service 返回的数据结构为 { total: number, list: Item[] }
const result = useRequest(service, {
// service 的第一个参数为 { current: number, pageSize: number }
defaultParams: [{ current: 1, pageSize: defaultPageSize }],
// refreshDeps 变化,会重置 current 到第一页,并重新发起请求,一般你可以把 pagination 依赖的条件放这里
refreshDepsAction: () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
changeCurrent(1);
},
...rest,
});
// 取到相关的请求参数
const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};
// 获取请求结果,total 代表数据总条数
const total = result.data?.total || 0;
// 获取到总的页数
const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
}
重点看下 onChange 方法:
- 入参分别为当前页数以及当前每一页的最大数量。
- 根据 total 算出总页数。
- 获取到所有的参数,执行请求逻辑。
- 当修改当前页或者当前每一页的最大数量的时候,直接调用 onChange 方法。
// c,代表 current page
// p,代表 page size
const onChange = (c: number, p: number) => {
let toCurrent = c <= 0 ? 1 : c;
const toPageSize = p <= 0 ? 1 : p;
// 根据 total 算出总页数
const tempTotalPage = Math.ceil(total / toPageSize);
// 假如此时总页面小于当前页面,需要将当前页面赋值为总页数
if (toCurrent > tempTotalPage) {
toCurrent = Math.max(1, tempTotalPage);
}
const [oldPaginationParams = {}, ...restParams] = result.params || [];
// 重新执行请求
result.run(
// 留意参数变化,主要是当前页数和每页的总数量发生变化
{
...oldPaginationParams,
current: toCurrent,
pageSize: toPageSize,
},
...restParams,
);
};
const changeCurrent = (c: number) => {
onChange(c, pageSize);
};
const changePageSize = (p: number) => {
onChange(current, p);
};
最后返回请求的结果以及 pagination 字段,包含所有分页信息。另外还有操作分页的函数。
return {
...result,
// 会额外返回 pagination 字段,包含所有分页信息,及操作分页的函数。
pagination: {
current,
pageSize,
total,
totalPage,
onChange: useMemoizedFn(onChange),
changeCurrent: useMemoizedFn(changeCurrent),
changePageSize: useMemoizedFn(changePageSize),
},
} as PaginationResult<TData, TParams>;
小结:usePagination 默认用法与 useRequest 一致,但内部封装了分页请求相关的逻辑。返回的结果多返回一个 pagination 参数,包含所有分页信息,及操作分页的函数。
缺点就是对 API 请求参数有所限制,比如入参结构必须为 { current: number, pageSize: number },返回结果为 { total: number, list: Item[] }。
useAntdTable
useAntdTable 基于 useRequest 实现,封装了常用的 Ant Design Form 与 Ant Design Table 联动逻辑,并且同时支持 antd v3 和 v4。
首先调用 usePagination 处理分页的逻辑。
const useAntdTable = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: AntdTableOptions<TData, TParams> = {},
) => {
const {
// form 实例
form,
// 默认表单选项
defaultType = 'simple',
// 默认参数,第一项为分页数据,第二项为表单数据。[pagination, formData]
defaultParams,
manual = false,
// refreshDeps 变化,会重置 current 到第一页,并重新发起请求。
refreshDeps = [],
ready = true,
...rest
} = options;
// 对分页的逻辑进行处理
// 分页也是对 useRequest 的再封装
const result = usePagination<TData, TParams>(service, {
manual: true,
...rest,
});
// ...
}
然后处理列表页筛选 Form 表单的逻辑,这里支持 Antd v3 和 Antd v4 版本。
// 判断是否为 Antd 的第四版本
const isAntdV4 = !!form?.getInternalHooks;
获取当前表单值,form.getFieldsValue 或者 form.getFieldInstance:
// 获取当前的 from 值
const getActivetFieldValues = () => {
if (!form) {
return {};
}
// antd 4
if (isAntdV4) {
return form.getFieldsValue(null, () => true);
}
// antd 3
const allFieldsValue = form.getFieldsValue();
const activeFieldsValue = {};
Object.keys(allFieldsValue).forEach((key: string) => {
if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
activeFieldsValue[key] = allFieldsValue[key];
}
});
return activeFieldsValue;
};
校验表单逻辑 form.validateFields:
// 校验逻辑
const validateFields = (): Promise<Record<string, any>> => {
if (!form) {
return Promise.resolve({});
}
const activeFieldsValue = getActivetFieldValues();
const fields = Object.keys(activeFieldsValue);
// antd 4
// validateFields 直接调用
if (isAntdV4) {
return (form.validateFields as Antd4ValidateFields)(fields);
}
// antd 3
return new Promise((resolve, reject) => {
form.validateFields(fields, (errors, values) => {
if (errors) {
reject(errors);
} else {
resolve(values);
}
});
});
};
重置表单 form.setFieldsValue:
// 重置表单
const restoreForm = () => {
if (!form) {
return;
}
// antd v4
if (isAntdV4) {
return form.setFieldsValue(allFormDataRef.current);
}
// antd v3
const activeFieldsValue = {};
Object.keys(allFormDataRef.current).forEach((key) => {
if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
activeFieldsValue[key] = allFormDataRef.current[key];
}
});
form.setFieldsValue(activeFieldsValue);
};
修改表单类型,支持 'simple' 和 'advance'。初始化的表单数据可以填写 simple 和 advance 全量的表单数据,开发者可以根据当前激活的类型来设置表单数据。修改 type 的时候会重置 form 表单数据。
const changeType = () => {
// 获取当前表单值
const activeFieldsValue = getActivetFieldValues();
// 修改表单值
allFormDataRef.current = {
...allFormDataRef.current,
...activeFieldsValue,
};
// 设置表单类型
setType((t) => (t === 'simple' ? 'advance' : 'simple'));
};
// 修改 type,则重置 form 表单数据
useUpdateEffect(() => {
if (!ready) {
return;
}
restoreForm();
}, [type]);
_submit 方法:对 form 表单校验后,根据当前 form 表单数据、分页等筛选条件进行对表格数据搜索。
const _submit = (initPagination?: TParams[0]) => {
setTimeout(() => {
// 先进行校验
validateFields()
.then((values = {}) => {
// 分页的逻辑
const pagination = initPagination || {
pageSize: options.defaultPageSize || 10,
...(params?.[0] || {}),
current: 1,
};
// 假如没有 form,则直接根据分页的逻辑进行请求
if (!form) {
// @ts-ignore
run(pagination);
return;
}
// 获取到当前所有 form 的 Data 参数
// record all form data
allFormDataRef.current = {
...allFormDataRef.current,
...values,
};
// @ts-ignore
run(pagination, values, {
allFormData: allFormDataRef.current,
type,
});
})
.catch((err) => err);
});
};
另外当表格触发 onChange 方法的时候,也会进行请求:
// Table 组件的 onChange 事件
const onTableChange = (pagination: any, filters: any, sorter: any) => {
const [oldPaginationParams, ...restParams] = params || [];
run(
// @ts-ignore
{
...oldPaginationParams,
current: pagination.current,
pageSize: pagination.pageSize,
filters,
sorter,
},
...restParams,
);
};
初始化的时候,会根据当前是否有缓存的数据,有则根据缓存的数据执行请求逻辑。否则,通过 manual 和 ready 判断是否需要进行重置表单后执行请求逻辑。
// 初始化逻辑
// init
useEffect(() => {
// if has cache, use cached params. ignore manual and ready.
// params.length > 0,则说明有缓存
if (params.length > 0) {
// 使用缓存的数据
allFormDataRef.current = cacheFormTableData?.allFormData || {};
// 重置表单后执行请求
restoreForm();
// @ts-ignore
run(...params);
return;
}
// 非手动并且已经 ready,则执行 _submit
if (!manual && ready) {
allFormDataRef.current = defaultParams?.[1] || {};
restoreForm();
_submit(defaultParams?.[0]);
}
}, []);
最后,将请求返回的数据通过 dataSource、 pagination、loading 透传回给到 Table 组件,实现 Table 的数据以及状态的展示。以及将对 Form 表单的一些操作方法暴露给开发者。
return {
...result,
// Table 组件需要的数据,直接透传给 Table 组件即可
tableProps: {
dataSource: result.data?.list || defaultDataSourceRef.current,
loading: result.loading,
onChange: useMemoizedFn(onTableChange),
pagination: {
current: result.pagination.current,
pageSize: result.pagination.pageSize,
total: result.pagination.total,
},
},
search: {
// 提交表单
submit: useMemoizedFn(submit),
// 当前表单类型, simple | advance
type,
// 切换表单类型
changeType: useMemoizedFn(changeType),
// 重置当前表单
reset: useMemoizedFn(reset),
},
} as AntdTableResult<TData, TParams>;
边栏推荐
- 任务调度线程池基本介绍
- 技术栈概览
- 数据库单字段存储多个标签(位移操作)
- Little data on how to learn?Jida latest small learning data review, 26 PDF page covers the 269 - page document small data learning theory, method and application are expounded
- Simple test of the use of iptables
- 98.嵌入式控制器EC实战 EC开发板开发完成
- Failed to re-init queues : Illegal queue capacity setting (abs-capacity=0.6) > (abs-maximum-capacity
- 9月备考PMP,应该从哪里备考?
- LinkedList源码分享
- 有点奇怪!访问目的网址,主机能容器却不行
猜你喜欢

数据库内核面试中我不会的问题(1)

算法---解码方法(Kotlin)

Nacos 配置中心

idea插件generateAllSetMethod一键生成set/get方法以及bean对象转换

Postman 批量测试接口详细教程

Pytorch框架学习记录8——最大池化的使用

【个人作品】无线网络图传模块

WeChat applet cloud development | personal blog applet

MySQL语法基础

Little data on how to learn?Jida latest small learning data review, 26 PDF page covers the 269 - page document small data learning theory, method and application are expounded
随机推荐
Pytorch框架学习记录9——非线性激活
string
MySQL 中出现的字符编码错误 Incorrect string value: ‘\x\x\x\x‘ for column ‘x‘
自定义指令,获取焦点
system collection
密码学的基础:X.690和对应的BER CER DER编码
360借条安全专家:陌生微信好友不要轻易加贷款推广多是诈骗
基于FPGA的任意字节数(单字节、多字节)的串口(UART)发送(含源码工程)
通俗解释:什么是临床预测模型
win10版本1803无法升级1903系统如何解决
excel高级绘图技巧100讲(二十二)-如何对不规则数据进行分列
【节能学院】智能操控装置在高压开关柜的应用
使用百度EasyDL实现厂区工人抽烟行为识别
Nacos 配置中心
Pytorch框架学校记录11——搭建小实战完整细节
9月备考PMP,应该从哪里备考?
Little data on how to learn?Jida latest small learning data review, 26 PDF page covers the 269 - page document small data learning theory, method and application are expounded
【nn.Parameter()】生成和为什么要初始化
响应式织梦模板清洁服务类网站
OSG Notes: Set DO_NOT_COMPUTE_NEAR_FAR to manually calculate far and near planes