data-table-pro
v0.1.0
Published
A type-safe, compound React data table with built-in React Query integration. More flexible than antd Table.
Downloads
140
Maintainers
Readme
DataTable Pro
一个类型安全、基于复合组件模式的 React 数据表格库,内置 React Query 集成。
比 Ant Design Table 更灵活、更可控、类型推导更严格。
为什么用 DataTable Pro 而不是 Ant Design Table?
| 特性 | DataTable Pro | Ant Design Table |
|------|--------------|------------------|
| 类型安全 | 泛型 DataTable<T> 一路贯通到列定义、行数据、选中项,0 个 any | 大量 any / Record<string, any>,类型推导粗放 |
| 复合组件模式 | <Table> / <Header> / <Body> / <Row> / <Cell> / <Pagination> 可自由组合 | 单一 <Table> 组件,所有逻辑通过 Props 塞入 |
| 选中双模式 | 受控(外部 state)和非受控(内部 state)互不干扰,用 selectedRows === undefined 区分 | 仅受控,必须手动管理 |
| React Query 内置 | <WithQuery> 壳组件直接集成,useMemo 稳定 queryKey,杜绝无限请求 | 无内置支持,需自行集成 |
| 控件体积 | 按需加载,无额外 CSS 依赖(仅 Tailwind 基础类) | 全量引入 antd 样式,即使只用一个 Table |
| 源码可控 | 千行级源码,完全可读、可 debug、可定制 | 万行级黑盒,定制成本高 |
| 测试覆盖 | 核心逻辑 ≥ 90% 覆盖率,65%+ 分支覆盖 | 商业组件,测试不可见 |
设计权衡
- 不提供「开箱即用」的 UI 主题:DataTable Pro 只提供基础样式(Tailwind utility),方便你快速覆盖。如果需要精美主题,直接套用自己项目的 Tailwind 配置即可。
- 不内置「编辑表格」:聚焦在「展示型表格」场景,编辑功能留给用户通过
render自定义列实现。 - 不提供「虚拟滚动」:对于 1000+ 行数据,建议使用服务端分页 +
<WithQuery>方案。
安装
# pnpm(推荐)
pnpm add data-table-pro
# npm
npm install data-table-pro
# yarn
yarn add data-table-pro依赖要求
react≥ 18react-dom≥ 18@tanstack/react-query≥ 5(仅在使用<WithQuery>时需要)
快速上手
import { DataTable } from 'data-table-pro';
import type { Column } from 'data-table-pro';
interface User {
id: number;
name: string;
age: number;
role: string;
}
const columns: Column<User>[] = [
{ key: 'id', header: 'ID', sortable: true },
{ key: 'name', header: 'Name', sortable: true, filter: { type: 'text' } },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'role', header: 'Role', filter: { type: 'select' } },
];
const data: User[] = [
{ id: 1, name: 'Alice', age: 30, role: 'engineer' },
{ id: 2, name: 'Bob', age: 25, role: 'designer' },
{ id: 3, name: 'Carol', age: 35, role: 'qa' },
];
export default function App() {
return <DataTable<User> data={data} columns={columns} />;
}完整 API 文档
DataTable<T> 主组件
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| data | readonly T[] | 必填 | 表格数据 |
| columns | readonly Column<T>[] | 必填 | 列定义 |
| selectedRows | readonly T[] | undefined | 传入即受控模式 |
| onSelectionChange | (rows: readonly T[]) => void | - | 选中变化回调 |
| enableSelection | boolean | 自动 | 是否显示复选框列 |
| getRowKey | (row: T) => string \| number | row.id 或 JSON.stringify | 行唯一标识 |
| pageSize | number | 10 | 每页行数 |
| children | ReactNode | - | 自定义子组件组合 |
Column<T> 列定义
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| key | keyof T | 是 | 数据字段键 |
| header | string | 是 | 表头显示文本 |
| sortable | boolean | 否 | 是否可排序 |
| filter | { type: 'text' \| 'select' \| 'range' } | 否 | 筛选配置 |
| render | (row: T) => ReactNode | 否 | 自定义渲染 |
| visible | boolean | 否 | 缺省为 true |
useDataTable<T> Hook
const {
processedData, // 排序+筛选+分页后的数据
totalCount, // 筛选后总条数
pageCount, // 总页数
sort, // 当前排序状态
filter, // 当前筛选状态
pagination, // 当前分页状态
selection, // 当前选中行
visibleColumns, // 可见列列表
api, // 操作方法
} = useDataTable<T>(options);api 操作方法
| 方法 | 签名 | 说明 |
|------|------|------|
| toggleSort | (key: keyof T) => void | 三态切换排序 |
| setFilter | (key: keyof T, value: unknown) => void | 设置筛选值 |
| clearFilter | (key: keyof T) => void | 清除某字段筛选 |
| setPage | (page: number) => void | 跳转页码 |
| setPageSize | (size: number) => void | 修改每页行数 |
| toggleColumnVisibility | (key: keyof T) => void | 切换列显隐 |
| toggleSelect | (row: T) => void | 选中/取消某行 |
| toggleSelectAll | () => void | 全选/反选当前页 |
| setSelection | (rows: readonly T[]) => void | 精确设置选中行 |
| isRowSelected | (row: T) => boolean | 判断某行是否选中 |
| isAllSelected | () => boolean | 判断当前页是否全选 |
<WithQuery<T>> React Query 集成壳
import { useMemo } from 'react';
import { WithQuery } from 'data-table-pro';
import { QueryClientProvider } from '@tanstack/react-query';
const queryKey = useMemo(() => ['users', { page }], [page]);
<WithQuery<User>
queryKey={queryKey} // 必须用 useMemo 稳定引用(红线 #4)
fetcher={() => fetchUsers(page)}
enabled={true}
loadingFallback={<div>加载中...</div>}
errorFallback={(err, retry) => <button onClick={retry}>重试</button>}
>
{({ data, totalCount, isLoading, isFetching, error, refetch }) => (
<>
{isFetching && <span>刷新中...</span>}
<DataTable<User> data={data} columns={columns} />
</>
)}
</WithQuery>Props
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| queryKey | QueryKey | 必填 | React Query 的 key,必须用 useMemo 固定 |
| fetcher | () => Promise<WithQueryFetcherResult<T>> | 必填 | 数据拉取函数 |
| enabled | boolean | true | 是否启用查询 |
| loadingFallback | ReactNode | '加载中...' | 加载中 UI |
| errorFallback | (err: Error, retry: () => void) => ReactNode | - | 错误 UI |
| children | (props: WithQueryRenderProps<T>) => ReactNode | 必填 | 数据就绪后渲染 |
使用示例
1. 基础渲染
<DataTable<User> data={data} columns={columns} />2. 排序 + 筛选 + 分页
<DataTable<User> data={data} columns={columns} pageSize={5} />3. 非受控选中(多选)
<DataTable<User>
data={data}
columns={columns}
enableSelection
onSelectionChange={(rows) => console.log('选中:', rows)}
/>4. 受控选中 + 批量删除
function App() {
const [selected, setSelected] = useState<User[]>([]);
const deleteSelected = () => {
setData((prev) => prev.filter((u) => !selected.includes(u)));
setSelected([]);
};
return (
<>
<button onClick={deleteSelected}>删除选中 ({selected.length})</button>
<DataTable<User>
data={data}
columns={columns}
selectedRows={selected}
onSelectionChange={setSelected}
/>
</>
);
}5. 服务端分页 + 排序
const [page, setPage] = useState(1);
const [sortKey, setSortKey] = useState<'id' | 'name'>('id');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const queryKey = useMemo(
() => ['users', { page, sortKey, sortOrder }],
[page, sortKey, sortOrder],
);
const fetcher = useCallback(
() => fetchUsers({ page, sortKey, sortOrder }),
[page, sortKey, sortOrder],
);
<WithQuery<User> queryKey={queryKey} fetcher={fetcher}>
{({ data, totalCount }) => (
<DataTable<User> data={data} columns={columns} />
)}
</WithQuery>6. 复合组件自定义布局
<DataTable<User> data={data} columns={columns}>
<DataTable.Header />
<DataTable.Body>
<DataTable.Row index={0} />
<DataTable.Row index={1} />
</DataTable.Body>
<DataTable.Pagination />
</DataTable>红线约束
本组件库遵循以下核心设计原则(红线),违反会导致运行时错误或类型错误:
- 子组件须依附
<Table>:<Header>/<Body>/<Row>/<Cell>/<Pagination>单独渲染会抛出"[data-table-pro] 子组件必须在 <Table> 内使用"错误。 - 泛型链路贯通:
DataTable<T>→useDataTable<T>→Column<T>— 0 个any。 - 选中双模式互斥:
selectedRows === undefined走非受控内部 state,否则走受控,两种路径不共存。 - queryKey 必须
useMemo固定:<WithQuery>的queryKey若在渲染中新生成,会触发无限请求。 - 测试覆盖率 ≥ 60%:核心逻辑覆盖率 ≥ 90%,分支覆盖 ≥ 65%。
常见问题 (FAQ)
Q: 为什么我点击排序/筛选没有反应?
确保你传入了 sortable / filter 配置。useDataTable 的排序和筛选逻辑需要列定义中的对应字段。
Q: 选中行的复选框不显示?
需要设置 enableSelection 或传入 selectedRows / onSelectionChange。
Q: 受控选中模式下,为什么勾选后复选框不自动打勾?
受控模式要求父组件更新 selectedRows 值。勾选动作只触发 onSelectionChange 回调,不会修改内部 state。
Q: 如何修改表格样式?
表格使用 Tailwind utility 类,覆盖非常方便:
<DataTable<User>
data={data}
columns={columns}
className="your-custom-class"
/>或者直接覆盖 CSS:
/* 表格行悬停 */
tr:hover { background-color: #f0f9ff; }Q: 支持服务端排序/筛选吗?
支持。通过 <WithQuery> 把排序/筛选参数拼入 queryKey,每次变化自动触发新请求。
Q: 和 @tanstack/react-table 有什么区别?
@tanstack/react-table 是一个「无头 UI」库,你需要自己实现所有渲染逻辑。
DataTable Pro 提供开箱即用的复合组件,同时保留 useDataTable Hook 供自定义场景使用。
License
MIT
