在中后台项目中,最常见的页面不是表单,而是列表页。
真正消耗时间的,往往不是业务复杂度,而是分页、排序、loading、查询参数同步这些重复劳动。
这篇文章分享我在项目中封装的一个 Hook ------ useTableList,用于统一管理 Ant Design Table 的列表行为。
一、为什么要封装表格 Hook?
在没有封装之前,一个列表页通常要处理:
- loading 状态
- 分页 / 页码同步
- 查询参数合并
- 表格排序映射
- rowSelection 管理
- search / refresh / reset 行为区分
结果就是:
👉 每个列表页几乎都在复制粘贴,而且 bug 特别容易集中在这些地方。
所以我给自己定了一个目标:
列表页只关心:表单 + columns + 业务操作
其余全部交给 Hook。
二、设计目标
- ✅ 数据请求收敛到一个入口
- ✅ 表格行为(分页 / 排序)内聚
- ✅ 对外暴露语义清晰的 API
- ✅ 最大限度贴合 antd Table
- ✅ 可作为项目级基础设施
三、对外 API 设计
ts
const {
queryParams,
search,
refresh,
reset,
selectedRowKeys,
tableProps
} = useTableList(getListApi, { rowSelection: true })
search ------ 查询(回到第一页)
用于表单搜索 / 条件变化。
refresh ------ 刷新当前页
用于新增 / 删除 / 修改之后。
reset ------ 重置条件
用于重置按钮。
selectedRowKeys ------ 批量操作能力基础
tableProps ------ 直接传给 antd Table
tsx
<Table rowKey="id" columns={columns} {...tableProps} />
四、全局配置能力
解决不同后端字段不统一的问题:
ts
configureTableOption({
pageSize: 20,
sortField: ['orderType', 'orderField'],
sortOrder: ['ASC', 'DESC']
})
五、核心实现思路
1️⃣ 单一数据入口
所有行为最终都会走:
- search
- refresh
- reset
- 表格分页 / 排序
ts
const fetchList = async (params) => { ... }
这是整个 Hook 稳定性的核心。
2️⃣ 查询参数是唯一真相
分页、排序、条件都收敛在 queryParams 中,避免状态割裂。
3️⃣ 表格行为完全内聚
ts
onTableChange => fetchList()
页面层不再处理分页 / 排序细节。
4️⃣ rowSelection 统一托管
让批量操作天然可扩展。
六、完整源码(useTableList.ts)
ts
import { type Key, type ReactNode, useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { Empty, type TableProps as AntdTableProps } from 'antd'
import type {
TablePaginationConfig,
TableCurrentDataSource
} from 'antd/es/table/interface'
export interface QueryParamsData {
pageNo: number
pageSize: number
orderType?: string
orderField?: string
[key: string]: any
}
interface TableResponse<T> {
status: string
data: {
list: T[]
totalCount: number
}
}
interface TableState<T> {
pagination: TablePaginationConfig
list: T[]
queryParams: QueryParamsData
}
export interface TableProps<T> {
bordered: boolean
size: 'middle'
sticky: boolean
rowSelection: AntdTableProps<T>['rowSelection'] | undefined
pagination: TablePaginationConfig
loading: boolean
dataSource: T[]
onChange: AntdTableProps<T>['onChange']
locale: {
emptyText: string | ReactNode
}
}
interface TableListResult<T> {
/** 查询参数 */
queryParams: QueryParamsData
/** 执行查询方法 */
search: (params?: Record<string, any>) => void
refresh: (params?: Record<string, any>) => void
reset: (params?: Record<string, any>) => void
/** 选中的行 keys */
selectedRowKeys: Key[]
/** 表格属性 */
tableProps: TableProps<T>
}
interface GlobalTableConfig {
sortField: string[]
sortOrder: string[]
pageSize: number
}
// 默认配置
const globalTableConfig: GlobalTableConfig = {
sortField: ['orderType', 'orderField'],
sortOrder: ['ASC', 'DESC'],
pageSize: 10
}
/**
* 配置全局表格参数
* @param config
*/
export function configureTableOption(config: GlobalTableConfig): void {
Object.keys(config).forEach((key: string) => {
globalTableConfig[key] = config[key]
})
}
export function useTableList<T extends Record<string, any> = Record<string, any>>(
getRequestFn: (data: QueryParamsData) => Promise<TableResponse<T>>,
initParams = {} as Record<string, any>
): TableListResult<T> {
const { rowSelection, ...restInitParams } = initParams
const PAGE_SIZE = globalTableConfig.pageSize
const [state, setState] = useState<TableState<T>>({
pagination: {
showSizeChanger: true,
showQuickJumper: true,
total: 0,
pageSize: PAGE_SIZE,
current: 1
},
list: [],
queryParams: {
pageNo: 1,
pageSize: PAGE_SIZE,
...restInitParams
}
})
const { pagination, list, queryParams } = state
const { pageNo: currentPageNo, pageSize: currentPageSize } = queryParams
const [isLoading, setIsLoading] = useState(true)
const initialQuery = useRef(queryParams)
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([])
const rowSelectionData: AntdTableProps<T>['rowSelection'] | undefined = useMemo(() => {
if (!rowSelection) return void 0
return {
selectedRowKeys,
onChange: (keys: Key[]) => setSelectedRowKeys(keys)
}
}, [rowSelection, selectedRowKeys])
const showTotal = useCallback(
(total: number): string => {
return `共 ${total} 条记录 第 ${currentPageNo}/${Math.ceil(total / currentPageSize)} 页 `
},
[currentPageNo, currentPageSize]
)
const fetchList = useCallback(
async (params: QueryParamsData): Promise<void> => {
const { pageNo } = params
setIsLoading(true)
const queryParamsData = { ...restInitParams, pageSize: currentPageSize, ...params }
if (params.pageNo === void 0) {
queryParamsData.pageNo = 1
}
if (params.pageSize === void 0) {
queryParamsData.pageSize = currentPageSize
}
const { data } = await getRequestFn(queryParamsData)
const { list = [], totalCount = 0 } = data || {}
rowSelection && setSelectedRowKeys([])
setState({
list,
queryParams: queryParamsData as T & QueryParamsData,
pagination: {
...pagination,
current: pageNo,
pageSize: queryParamsData.pageSize,
total: totalCount
}
})
setIsLoading(false)
},
[queryParams, restInitParams, rowSelection, currentPageSize, getRequestFn, pagination]
)
const search = useCallback(
(params?: Record<string, any>) => {
fetchList({ ...queryParams, ...params, pageNo: 1 })
},
[fetchList, queryParams]
)
const refresh = useCallback(
(params?: Record<string, any>) => {
fetchList({ ...queryParams, ...params})
},
[fetchList, queryParams]
)
const reset = useCallback(
(params?: Record<string, any>) => {
fetchList({ ...params, pageSize: currentPageSize, pageNo: 1 })
},
[fetchList, currentPageSize]
)
const onTableChange = useCallback(
( pagination: TablePaginationConfig,
_filters: Record<string, any>,
sorter: Record<string, any>,
extra: TableCurrentDataSource<any>) => {
const { action } = extra
if (['paginate', 'sort'].includes(action)) {
const { current, pageSize } = pagination
const { field, order } = sorter
const [orderTypeField, orderFieldName] = globalTableConfig.sortField
const [ascValue, descValue] = globalTableConfig.sortOrder
const params = {
...queryParams,
[orderTypeField]: order ? (order === 'ascend' ? ascValue : descValue) : void 0,
[orderFieldName]: field,
pageNo: current as number,
pageSize: pageSize as number
}
fetchList(params)
}
},
[queryParams, fetchList]
)
const tableProps: TableProps<T> = useMemo(() => {
return {
bordered: true,
size: 'middle',
sticky: true,
rowSelection: rowSelectionData,
pagination: { ...pagination, showTotal },
loading: isLoading,
dataSource: list,
onChange: onTableChange,
locale: {
emptyText: isLoading ? '' : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
}
}
}, [rowSelectionData, pagination, showTotal, isLoading, list, onTableChange])
useEffect(() => {
search(initialQuery.current)
}, [])
return {
queryParams,
search,
refresh,
reset,
selectedRowKeys,
tableProps
}
}
七、页面使用示例
ts
const { tableProps, search, reset, selectedRowKeys } = useTableList(getUserList, {
rowSelection: true
})
tsx
<Form onFinish={search}>
<Button htmlType="submit">搜索</Button>
<Button onClick={reset}>重置</Button>
</Form>
<Table rowKey="id" columns={columns} {...tableProps} />
八、总结
useTableList 带来的不是"少写几行代码",而是:
- 列表页工程化
- 行为模型统一
- Bug 集中收口
- 可持续扩展
非常适合作为中后台项目的基础设施。
九、后续可进阶方向
- URL 同步查询参数
- 导出 / 批量操作能力
- 自动轮询 / 缓存
- 向 ProTable 形态演进
如果这篇文章对你有帮助,欢迎点赞 / 收藏 / 评论交流 👏 也欢迎分享你们项目中是如何封装列表页的。