列表頁(yè)常見hook封裝實(shí)例
引言
本文是深入淺出 ahooks 源碼系列文章,這個(gè)系列的目標(biāo)主要有以下幾點(diǎn):
- 加深對(duì) React hooks 的理解。
- 學(xué)習(xí)如何抽象自定義 hooks。構(gòu)建屬于自己的 React hooks 工具庫(kù)。
- 培養(yǎng)閱讀學(xué)習(xí)源碼的習(xí)慣,工具庫(kù)是一個(gè)對(duì)源碼閱讀不錯(cuò)的選擇。
列表頁(yè)常見元素
對(duì)于一些后臺(tái)管理系統(tǒng),典型的列表頁(yè)包括篩選表單項(xiàng)、Table表格、Pagination分頁(yè)這三部分。
針對(duì)使用 Antd 的系統(tǒng),在 ahooks 中主要是通過 useAntdTable 和 usePagination 這兩個(gè) hook 來封裝。
usePagination
usePagination 基于 useRequest 實(shí)現(xiàn),封裝了常見的分頁(yè)邏輯。
首先通過 useRequest 處理請(qǐng)求,service 約定返回的數(shù)據(jù)結(jié)構(gòu)為 { total: number, list: Item[] }。
其中 useRequest 的 defaultParams 參數(shù)第一個(gè)參數(shù)為 { current: number, pageSize: number }。并根據(jù)請(qǐng)求的參數(shù)以及返回的 total 值,得出總的頁(yè)數(shù)。
還有 refreshDeps 變化,會(huì)重置 current 到第一頁(yè)「changeCurrent(1)」,并重新發(fā)起請(qǐng)求,一般你可以把 pagination 依賴的條件放這里。
const usePagination = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: PaginationOptions<TData, TParams> = {},
) => {
const { defaultPageSize = 10, ...rest } = options;
// service 返回的數(shù)據(jù)結(jié)構(gòu)為 { total: number, list: Item[] }
const result = useRequest(service, {
// service 的第一個(gè)參數(shù)為 { current: number, pageSize: number }
defaultParams: [{ current: 1, pageSize: defaultPageSize }],
// refreshDeps 變化,會(huì)重置 current 到第一頁(yè),并重新發(fā)起請(qǐng)求,一般你可以把 pagination 依賴的條件放這里
refreshDepsAction: () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
changeCurrent(1);
},
...rest,
});
// 取到相關(guān)的請(qǐng)求參數(shù)
const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};
// 獲取請(qǐng)求結(jié)果,total 代表數(shù)據(jù)總條數(shù)
const total = result.data?.total || 0;
// 獲取到總的頁(yè)數(shù)
const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
}
重點(diǎn)看下 onChange 方法:
- 入?yún)⒎謩e為當(dāng)前頁(yè)數(shù)以及當(dāng)前每一頁(yè)的最大數(shù)量。
- 根據(jù) total 算出總頁(yè)數(shù)。
- 獲取到所有的參數(shù),執(zhí)行請(qǐng)求邏輯。
- 當(dāng)修改當(dāng)前頁(yè)或者當(dāng)前每一頁(yè)的最大數(shù)量的時(shí)候,直接調(diào)用 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;
// 根據(jù) total 算出總頁(yè)數(shù)
const tempTotalPage = Math.ceil(total / toPageSize);
// 假如此時(shí)總頁(yè)面小于當(dāng)前頁(yè)面,需要將當(dāng)前頁(yè)面賦值為總頁(yè)數(shù)
if (toCurrent > tempTotalPage) {
toCurrent = Math.max(1, tempTotalPage);
}
const [oldPaginationParams = {}, ...restParams] = result.params || [];
// 重新執(zhí)行請(qǐng)求
result.run(
// 留意參數(shù)變化,主要是當(dāng)前頁(yè)數(shù)和每頁(yè)的總數(shù)量發(fā)生變化
{
...oldPaginationParams,
current: toCurrent,
pageSize: toPageSize,
},
...restParams,
);
};
const changeCurrent = (c: number) => {
onChange(c, pageSize);
};
const changePageSize = (p: number) => {
onChange(current, p);
};
最后返回請(qǐng)求的結(jié)果以及 pagination 字段,包含所有分頁(yè)信息。另外還有操作分頁(yè)的函數(shù)。
return {
...result,
// 會(huì)額外返回 pagination 字段,包含所有分頁(yè)信息,及操作分頁(yè)的函數(shù)。
pagination: {
current,
pageSize,
total,
totalPage,
onChange: useMemoizedFn(onChange),
changeCurrent: useMemoizedFn(changeCurrent),
changePageSize: useMemoizedFn(changePageSize),
},
} as PaginationResult<TData, TParams>;
小結(jié):usePagination 默認(rèn)用法與 useRequest 一致,但內(nèi)部封裝了分頁(yè)請(qǐng)求相關(guān)的邏輯。返回的結(jié)果多返回一個(gè) pagination 參數(shù),包含所有分頁(yè)信息,及操作分頁(yè)的函數(shù)。
缺點(diǎn)就是對(duì) API 請(qǐng)求參數(shù)有所限制,比如入?yún)⒔Y(jié)構(gòu)必須為 { current: number, pageSize: number },返回結(jié)果為 { total: number, list: Item[] }。
useAntdTable
useAntdTable 基于 useRequest 實(shí)現(xiàn),封裝了常用的 Ant Design Form 與 Ant Design Table 聯(lián)動(dòng)邏輯,并且同時(shí)支持 antd v3 和 v4。
首先調(diào)用 usePagination 處理分頁(yè)的邏輯。
const useAntdTable = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: AntdTableOptions<TData, TParams> = {},
) => {
const {
// form 實(shí)例
form,
// 默認(rèn)表單選項(xiàng)
defaultType = 'simple',
// 默認(rèn)參數(shù),第一項(xiàng)為分頁(yè)數(shù)據(jù),第二項(xiàng)為表單數(shù)據(jù)。[pagination, formData]
defaultParams,
manual = false,
// refreshDeps 變化,會(huì)重置 current 到第一頁(yè),并重新發(fā)起請(qǐng)求。
refreshDeps = [],
ready = true,
...rest
} = options;
// 對(duì)分頁(yè)的邏輯進(jìn)行處理
// 分頁(yè)也是對(duì) useRequest 的再封裝
const result = usePagination<TData, TParams>(service, {
manual: true,
...rest,
});
// ...
}
然后處理列表頁(yè)篩選 Form 表單的邏輯,這里支持 Antd v3 和 Antd v4 版本。
// 判斷是否為 Antd 的第四版本 const isAntdV4 = !!form?.getInternalHooks;
獲取當(dāng)前表單值,form.getFieldsValue 或者 form.getFieldInstance:
// 獲取當(dāng)前的 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;
};
校驗(yàn)表單邏輯 form.validateFields:
// 校驗(yàn)邏輯
const validateFields = (): Promise<Record<string, any>> => {
if (!form) {
return Promise.resolve({});
}
const activeFieldsValue = getActivetFieldValues();
const fields = Object.keys(activeFieldsValue);
// antd 4
// validateFields 直接調(diào)用
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'。初始化的表單數(shù)據(jù)可以填寫 simple 和 advance 全量的表單數(shù)據(jù),開發(fā)者可以根據(jù)當(dāng)前激活的類型來設(shè)置表單數(shù)據(jù)。修改 type 的時(shí)候會(huì)重置 form 表單數(shù)據(jù)。
const changeType = () => {
// 獲取當(dāng)前表單值
const activeFieldsValue = getActivetFieldValues();
// 修改表單值
allFormDataRef.current = {
...allFormDataRef.current,
...activeFieldsValue,
};
// 設(shè)置表單類型
setType((t) => (t === 'simple' ? 'advance' : 'simple'));
};
// 修改 type,則重置 form 表單數(shù)據(jù)
useUpdateEffect(() => {
if (!ready) {
return;
}
restoreForm();
}, [type]);
_submit 方法:對(duì) form 表單校驗(yàn)后,根據(jù)當(dāng)前 form 表單數(shù)據(jù)、分頁(yè)等篩選條件進(jìn)行對(duì)表格數(shù)據(jù)搜索。
const _submit = (initPagination?: TParams[0]) => {
setTimeout(() => {
// 先進(jìn)行校驗(yàn)
validateFields()
.then((values = {}) => {
// 分頁(yè)的邏輯
const pagination = initPagination || {
pageSize: options.defaultPageSize || 10,
...(params?.[0] || {}),
current: 1,
};
// 假如沒有 form,則直接根據(jù)分頁(yè)的邏輯進(jìn)行請(qǐng)求
if (!form) {
// @ts-ignore
run(pagination);
return;
}
// 獲取到當(dāng)前所有 form 的 Data 參數(shù)
// record all form data
allFormDataRef.current = {
...allFormDataRef.current,
...values,
};
// @ts-ignore
run(pagination, values, {
allFormData: allFormDataRef.current,
type,
});
})
.catch((err) => err);
});
};
另外當(dāng)表格觸發(fā) onChange 方法的時(shí)候,也會(huì)進(jìn)行請(qǐng)求:
// 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,
);
};
初始化的時(shí)候,會(huì)根據(jù)當(dāng)前是否有緩存的數(shù)據(jù),有則根據(jù)緩存的數(shù)據(jù)執(zhí)行請(qǐng)求邏輯。否則,通過 manual 和 ready 判斷是否需要進(jìn)行重置表單后執(zhí)行請(qǐng)求邏輯。
// 初始化邏輯
// init
useEffect(() => {
// if has cache, use cached params. ignore manual and ready.
// params.length > 0,則說明有緩存
if (params.length > 0) {
// 使用緩存的數(shù)據(jù)
allFormDataRef.current = cacheFormTableData?.allFormData || {};
// 重置表單后執(zhí)行請(qǐng)求
restoreForm();
// @ts-ignore
run(...params);
return;
}
// 非手動(dòng)并且已經(jīng) ready,則執(zhí)行 _submit
if (!manual && ready) {
allFormDataRef.current = defaultParams?.[1] || {};
restoreForm();
_submit(defaultParams?.[0]);
}
}, []);
最后,將請(qǐng)求返回的數(shù)據(jù)通過 dataSource、 pagination、loading 透?jìng)骰亟o到 Table 組件,實(shí)現(xiàn) Table 的數(shù)據(jù)以及狀態(tài)的展示。以及將對(duì) Form 表單的一些操作方法暴露給開發(fā)者。
return {
...result,
// Table 組件需要的數(shù)據(jù),直接透?jìng)鹘o 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),
// 當(dāng)前表單類型, simple | advance
type,
// 切換表單類型
changeType: useMemoizedFn(changeType),
// 重置當(dāng)前表單
reset: useMemoizedFn(reset),
},
} as AntdTableResult<TData, TParams>;以上就是列表頁(yè)常見 hook 封裝示例的詳細(xì)內(nèi)容,更多關(guān)于列表頁(yè) hook 封裝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react如何修改循環(huán)數(shù)組對(duì)象的數(shù)據(jù)
這篇文章主要介紹了react如何修改循環(huán)數(shù)組對(duì)象的數(shù)據(jù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
React中useTransition鉤子函數(shù)的使用詳解
React?18的推出標(biāo)志著React并發(fā)特性的正式到來,其中useTransition鉤子函數(shù)是一個(gè)重要的新增功能,下面我們就來學(xué)習(xí)一下useTransition鉤子函數(shù)的具體使用吧2024-02-02
react學(xué)習(xí)筆記之state以及setState的使用
這篇文章主要介紹了react學(xué)習(xí)筆記之state以及setState的使用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
react腳手架構(gòu)建運(yùn)行時(shí)報(bào)錯(cuò)問題及解決
這篇文章主要介紹了react腳手架構(gòu)建運(yùn)行時(shí)報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03

