目录
引言列表页常见元素usePaginationuseAntdTable
引言
本文是深入浅出 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>;
复制代码以上就是列表页常见 hook 封装示例的详细内容,更多关于列表页 hook 封装的资料请关注中国红客联盟其它相关文章! |