封装react hook之useTable
重复的逻辑
在做前端开发中表格的渲染、分页、勾选的功能是十分常见的。在实际开发中我们经常使用 Ant Design for react 的 UI 库。它已经帮我们封装了table表格组件、Pagination分页组件,提升了我们的开发效率。
但是,我们依然需要自己去维护dataSource、pagination、rowSelection等状态。
我们可能会写出如下代码:
import React, {useCallback, useEffect, useState} from "react";
import {Table} from "antd";
import {getTableDataRequset} from "../../api";
const Page = () => {
const [loading, setLoading] = useState(false);
const [pageNum, setPageNum] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(100);
const [counts, setCounts] = useState<number>(0);
const [dataSource, setDataSource] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
// 获取table数据
const getTableData = useCallback(async (index: number, size: number) => {
setLoading(true);
const {result, total} = await getTableDataRequset({pageIndex: index, pageSize: size});
setDataSource(result);
setLoading(false);
setCounts(total);
setSelectedRowKeys([]);
}, []);
const handleChangePage = (current: number, size: number) => {
setPageNum(current);
setPageSize(size);
getTableData(current, size);
};
const onSelectChange = (keys) => {
setSelectedRowKeys(keys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
useEffect(() => {
// 初始化
getTableData(1, 10);
}, [getTableData]);
return (
<Table
columns={[]}
loading={loading}
pagination={{
pageSize,
total: counts,
current: pageNum,
onChange: handleChangePage,
showSizeChanger: true,
pageSizeOptions: ["10", "20", "50"]
}}
rowSelection={rowSelection}
dataSource={dataSource}
/>
);
};
而如果包含关键字搜索功能,我们在搜索的时候,还需要重置一下当前页数、勾选项等状态...
在删除表格数据后,刷新表格是否刷新当前页,还是前一页(当前页数据删除完的情况下,需要刷新的是前一页)
...
我们可以发现这些逻辑是重复的、且是不可少的。我们可以使用hooks封装起来。减少这样的重复代码,提升效率,让我们更加专注在业务开发上。
设计一个useTalbe
useTable
主要用于封装常用的分页逻辑、表格勾选逻辑、表格数据请求逻辑。useTable
接收2个参数,requsetService
是表格列表的promise请求函数、options
是useTable的配置对象useTable
会返回tableProps
、tableSelectRows、table状态的一些操作函数:searchTable
clearTable
clearSelectedRows
refreshTable
const options = {
isInitTable: false,
initRequsetParams: {},
isRememberSelect: false
}
const {tableProps, searchTable} = useTable(getTableList, options);
requsetService
useTable
接收的第一个参数,表格列表的promise请求函数
// 接口返回的数据结构
interface Result<RecordType=any> {
/** 分页列表 */
result: RecordType[];
/** 总数 */
totalCount: number;
}
// 分页参数字段
interface ReuqsetParams {
/** 分页器之外的其他字段 */
[key: string]: any;
/** 当前页码 */
pageIndex: number;
/** 每页数量 */
pageSize: number;
}
type RequsetService = (params?: ReuqsetParams) => Promise<Result>;
options
useTable
接收的第二个参数,optons
是useTable
的配置对象
interface Opitons {
/** 是否初始化table,默认为false,主动请求使用searchTable */
isInitTable?: boolean;
/** 请求初始化参数 */
initRequsetParams?: object;
/** 翻页是否记住勾选的状态 */
isRememberSelect?: boolean;
}
返回值
interface SearchOptions {
/** 是否缓存请求参数的状态,默认true */
isCacheParams: boolean;
}
interface ReturnTable<RecordType, Params> {
/** Table组件需要的的props,直接传给Table组件 */
tableProps: {
dataSource: RecordType[];
loading: boolean;
pagination: TablePaginationConfig;
rowSelection: TableRowSelection<RecordType>;
};
/** 选中的行 */
tableSelectRows: RecordType[];
/** 接收搜索关键字搜索列表,分页会重置到第一页 */
searchTable: (params?: Params, searchOptions?: SearchOptions) => void;
/**
根据上次请求参数,刷新当前页列表。(适用于新增、编辑、删除刷新列表)
removeCount(默认0) 为删除的数量,删除列表数据时,如果当前页删除完需要请求上一页
*/
refreshTable: (options?: {removeCount?: number}) => void;
/** 清空表格数据方法 */
clearTable: () => void;
/** 清除选中项方法 */
clearSelectedRows: () => void;
}
使用场景
只需要分页器,不需要勾选项
const View = () => {
const {
tableProps,
searchTable,
} = useTable(getTableList);
// 初始化表格
useEffect(() => {
searchTable();
}, [searchTable]);
return (
<>
<Table
{...tableProps}
// 设置为null
rowSelection={null}
columns={tableColumns}
/>
</>
)
};
使用searchTable和refreshTable刷新表格
const View = () => {
// 返回了table的状态及操作方法,根据需求使用
const {
tableProps,
tableSelectRows,
searchTable,
refreshTable,
clearTable,
clearSelectedRows
} = useTable(getTableList);
// 刷新列表
// 会使用上一次的搜索参数
const handleRefresh = () => {
refreshTable();
};
// 搜索列表
// 传入搜索参数
const handleSearch = () => {
searchTable({
keyword: "111",
projectId: 1,
// ...
});
// xxx.api?keyword=111&projectId=1
};
// 初始化表格
useEffect(() => {
searchTable();
}, [searchTable]);
return (
<>
<button onClick={handleRefresh}>刷新列表</button>
<button onClick={handleSearch}>搜索列表</button>
<Table
{...tableProps}
columns={tableColumns}
/>
</>
)
};
实现useTalbe
我们根据设计的api实现一下该hook utils文件
// 获取勾选项(翻页记忆)
export const getSelectedRows = <T>(curPageSelectedKeys: rowKey[], curPageData: T[], selectedRows, rowKey = "id"): Result<T> => {
const dataMap = new Map<string|number, T>([]);
const nextRows = [];
// 当前页勾选项 + 其他分页已勾选项
curPageData.forEach((item) => {
dataMap.set(item[rowKey], item);
if (curPageSelectedKeys.includes(item[rowKey])) {
// 当前页勾选项
nextRows.push(item);
}
});
selectedRows.forEach((item) => {
if (dataMap.get(item[rowKey]) === undefined) {
// 其他分页已勾选项
nextRows.push(item);
}
});
const isAll = curPageSelectedKeys.length === curPageData.length;
return {
selectedRows: nextRows,
selectedKeys: nextRows.map((item) => item[rowKey]),
isAllSelected: isAll
};
};
useTable文件
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import React, {useEffect, useState, useRef, useCallback} from "react";
import {TablePaginationConfig} from "antd/lib/table";
import ReactDOM from "react-dom";
import {getSelectedRows} from "../../utils";
interface Result<RecordType> {
result: RecordType[];
totalCount: number;
}
type Service = (params?: object) => Promise<Result<any>>;
type Key = React.Key;
// 提取列表项的类型
type ServiceResponse<S> = S extends (params?: any) => Promise<infer Response>
? Response extends Result<infer RecordType>
? RecordType
: any
: never;
// 提取接口请求参数的类型
type ServiceParams<S> = S extends (params: infer Params) => Promise<any>
? Partial<Omit<Params, "pageIndex" | "pageSize">>
: never;
interface Opitons<Params> {
isInitTable?: boolean;
initRequsetParams?: Params;
isRememberSelect?: boolean;
}
interface Selected<RecordType> {
rows: RecordType[];
rowKeys: Key[];
}
export interface TableRowSelection<RecordType> {
selectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: RecordType[]) => void;
}
interface SearchOptions {
isCacheParams: boolean;
}
interface ReturnTable<RecordType, Params> {
tableProps: {
dataSource: RecordType[];
loading: boolean;
pagination: TablePaginationConfig;
rowSelection: TableRowSelection<RecordType>;
};
searchTable: (params?: Params, searchOptions?: SearchOptions) => void;
clearTable: () => void;
clearSelectedRows: () => void;
refreshTable: (options?: {removeCount?: number}) => void;
tableSelectRows: RecordType[];
}
const defaultPagination = {
current: 1,
pageSize: 10,
total: 0,
};
const defaultSelected = {
rows: [],
rowKeys: [],
};
const defalutOptions = {
isInitTable: false, // 是否初始化table
initRequsetParams: {},
isRememberSelect: false, // 是否记忆翻页勾选
};
/**
* @template S
* @param {Service} service 请求接口
* @param {Opitons} [options=defalutOptions] 初始化参数
* @return {*} {ReturnTable<RecordType, Params>}
*/
const useTable = <S extends Service, RecordType = ServiceResponse<S>, Params = ServiceParams<S>>(
service: S,
options: Opitons<Params> = defalutOptions as Opitons<Params>
): ReturnTable<RecordType, Params> => {
const [isUpdate, setIsUpdate] = useState(false); // 是否更新数据
const [dataSource, setDataSource] = useState<RecordType[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState(defaultPagination); // 分页配置参数
const [selected, setSelected] = useState<Selected<RecordType>>(defaultSelected); // 选择的条目
const requsetParams = useRef(options.initRequsetParams); // 请求参数
const isInitTable = useRef(options.isInitTable); // 是否初始化table
const isRememberSelect = useRef(options.isRememberSelect); // 是否记忆翻页勾选
const requsetService = useRef(service); // 分页列表请求
// 接收请求参数,根据参数搜索列表,初始化分页器
const searchTable = useCallback((
searchParams: Params = {} as Params,
searchOptions: SearchOptions = {isCacheParams: true}
) => {
if (!isInitTable.current) {
isInitTable.current = true;
}
// 使用上一次请求的缓存
if (searchOptions.isCacheParams) {
requsetParams.current = {
...requsetParams.current,
...searchParams
};
} else {
requsetParams.current = {
...searchParams
};
}
ReactDOM.unstable_batchedUpdates(() => {
// 在非第一页 跳转到第1页 进行搜索
setPagination((prevState) => ({
...prevState,
current: defaultPagination.current,
}));
// 强制刷新
setIsUpdate((prevState) => !prevState);
});
}, []);
// // 根据上次请求参数,刷新当前页列表
const refreshTable = useCallback((refreshOptions = {removeCount: 0}) => {
const {removeCount} = refreshOptions;
// 总数量-删除数量 /每页条数 = 总页 >= 当前页 刷新当前页
const afterTotalPage = Math.ceil((pagination.total - removeCount) / pagination.pageSize);
if (afterTotalPage >= pagination.current || afterTotalPage === 0) {
setIsUpdate((prevState) => !prevState);
} else {
ReactDOM.unstable_batchedUpdates(() => {
setPagination((prevState) => ({
...prevState,
current: afterTotalPage,
}));
// 强制刷新
setIsUpdate((prevState) => !prevState);
});
}
}, [pagination]);
// 清空列表
const clearTable = useCallback(() => {
setDataSource([]);
setPagination(defaultPagination);
setSelected(defaultSelected);
}, []);
// 清除选中项
const clearSelectedRows = useCallback(() => {
setSelected(defaultSelected);
}, []);
// 翻页
const onPageChange = (page: number, pageSize: number) => {
setPagination({
...pagination,
current: page,
pageSize,
});
};
// 表格选择
const onSelectChange = (selectedRowKeys: Key[], selectedRows: RecordType[]) => {
if (isRememberSelect.current) {
const {
selectedRows: rememberSelectedRows,
selectedKeys: rememberSelectedkeys,
} = getSelectedRows(selectedRowKeys, dataSource, selected.rows);
setSelected({
rows: rememberSelectedRows,
rowKeys: rememberSelectedkeys,
});
} else {
setSelected({
rows: selectedRows,
rowKeys: selectedRowKeys,
});
}
};
const rowSelection = {
selectedRowKeys: selected.rowKeys,
onChange: onSelectChange,
};
const paginationConfig = {
...pagination,
position: ["bottomRight" as "bottomRight"],
pageSizeOptions: ["10", "20", "30", "40", "50"],
showSizeChanger: true,
showQuickJumper: true,
onChange: onPageChange,
showTotal(totals: number, _range: [number, number]) {
return `共 ${totals}条 , 共 ${Math.ceil(totals / pagination.pageSize)}页`;
},
};
useEffect(() => {
if (!isInitTable.current) {
return;
}
setLoading(true);
const tableRequsetParams = {
...requsetParams.current,
pageIndex: pagination.current,
pageSize: pagination.pageSize
};
if (!isRememberSelect.current) {
// 初始化选择器
setSelected(defaultSelected);
}
requsetService.current(tableRequsetParams)
.then((res) => {
setLoading(false);
setDataSource(res.result);
setPagination({
...pagination,
total: res.totalCount,
});
})
.catch((err) => {
setLoading(false);
});
}, [pagination.current, pagination.pageSize, isUpdate]);
return {
tableProps: {
dataSource,
loading,
pagination: paginationConfig,
rowSelection
},
searchTable,
refreshTable,
clearTable,
clearSelectedRows,
tableSelectRows: selected.rows
};
};
export default useTable;
转载自:https://juejin.cn/post/7232460999189708861