Solve Any Data Table Problem using TanStack Table

TanStack Table 拯救前端复杂的数据表格呈现问题

前言

前端生涯中遇到无数攸关资料呈现的需求需要解决,而 TanStack Table🔗 是一款第一眼看起来很多余复杂但实际上能省下无数时间的一个优质套件,如果有任何「大量表单、中大型表单应用」我会强烈推荐。

在实际工作中我通过导入 TanStack Table 在几天时间内就能把现有表单大多复杂功能重现 ,并开始大量制作可重用、扩展性高且简洁的数据表单,因为它:

  • Headless UI 库,意味着它不绑定任何样式,只提供状态与逻辑,可以与任何现有界面进行整合。
  • 提供各大主流框架(React、Vue、Angular)适配器。
  • 免费开源且热门🔗,范例众多且文档完善清晰,支持 TypeScript。

数据表单背后的复杂

显示数据到表格上没什么困难的对吧?最简单的表单就是拿到数据 → 渲染上画面 → 保持数据更新,但实际上还有很多问题需要考虑,像是兼顾维护客户端与服务器端状态与逻辑(加载数据、分页、搜索、排序……)就足够头痛。

由于没有事先规划,导致有一次一项微小的功能改动(分页笔数数量调整)都要耗费 3 个工作天,对上百个数据表单进行散弹手术🔗式的修改,既没效率又不可靠,当专案越做越大、功能越加越多,会发现自己正在重刻一套「小型的表格框架」,而且代码相互耦合 Bug 不断。

TanStack Table 核心概念

初次见到以下概念与 API 可能会觉得很复杂,因为它的定位偏向是一款需要自己配置的工具箱,接上自己的 UI 会需要付出额外的时间和经验,初入门可以参考像是 Shadcn 有不错的文档与逐步范例 TanStack Table 整合🔗

实际范例

以建立一个客户端表单 Vue ClientDataTable.vue 为案例,最重要可以看到它接收 columns(列定义)与 data (表单数据),并且在内部定义完善数据状态与更新流程。

<ClientSideDataTable
:columns="thingsColumns"
:data="thingsData"
:is-loading="isThingsFetching">
<template #header="{ rowSelection }" >
<Button @click="() => handleActionA(rowSelection)" >Action A</Button>
<Button>Action B</Button>
<Button>Action C</Button>
</template>
<template #filter="{ table }">
<DataTableViewOptions :table="table"/>
<DataTableSearch
:table="table"
placeholder="搜索栏位⋯⋯"
filtering-column="email"
/>
</template>
</ClientSideDataTable>

客户端表单实践起来大致会像底下这样,预留了一个 header slot 暴露选取状态与 filter slot 暴露表单状态:

<template>
<div>
<div v-if="$slots.header">
<!-- 功能选单 -->
<slot name="header" :row-selection="rowSelection" />
</div>
<div v-if="$slots.filter">
<!-- 过滤选单 -->
<slot name="filter" :table="table" />
</div>
<!-- 数据表渲染 -->
<DataTable :table="table" :columns="columns" :is-loading="isLoading" />
</div>
<!-- 数据表分页 -->
<DataTablePagination :table="table" />
</template>
<script setup lang="ts" generic="TData, TValue">
import DataTable from '@/components/ui/datatables/DataTable.vue'
import {
useVueTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
getExpandedRowModel,
type SortingState,
type ColumnFiltersState,
type ColumnDef,
type VisibilityState,
type ExpandedState,
type RowSelectionState,
} from '@tanstack/vue-table'
import { valueUpdater } from '@/utils/dataTable'
const { data, columns, isLoading } = defineProps<{
columns: ColumnDef<TData, TValue>[];
data?: TData[];
isLoading: boolean;
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref<RowSelectionState>({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
get data() { return data ?? [] },
get columns() { return columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: (updaterOrValue) => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: (updaterOrValue) => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: (updaterOrValue) => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: (updaterOrValue) => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: (updaterOrValue) => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
</script>

更麻烦的服务器表单问题在于状态都来自表单外部,这时候如果通过先前提到的「如何通过 TanStack Query 构建更好的前端数据获取体验」整合 TanStack Query 把拿到的状态与表单分页、排序与搜索……等计算逻辑封装出一个 useServerSideDataTable Composable 如下:

interface UseServerSideDataTableOptions<TResult> {
queryKey: QueryKey;
queryFn: (params: {
internalTableFilter: {
limit: number;
page: number;
sortBy: string;
sortDesc: boolean;
search: string;
};
}) => Promise<{
totalRows: number;
results: TResult[];
}>;
}
export function useServerSideDataTable<TResult>(
options: UseServerSideDataTableOptions<TResult>,
) {
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const tableFilter = computed(() => {
// 分页系统索引起点差异需要转换
const zeroToOneBasedIndexOffset = 1
const firstSortingId = sorting.value[0]?.id
const firstSortDesc = sorting.value[0]?.desc
// TanStack Table column 字段不一定是 string,可能是数字、日期、选单,但我们应该只会用到 string 搜索,所以先断言为 string
const firstSearch = columnFilters.value[0]?.value as string
return {
limit: pagination.value.pageSize,
page: pagination.value.pageIndex + zeroToOneBasedIndexOffset,
sortBy: firstSortingId,
sortDesc: firstSortDesc,
search: firstSearch,
}
})
const query = useQuery({
queryKey: [...options.queryKey, pagination, sorting, columnFilters],
queryFn: () =>
options.queryFn({
internalTableFilter: tableFilter.value,
}),
placeholderData: keepPreviousData,
})
const pageCount = computed(() => {
const queryData = query.data.value
if (!queryData) return 0
const totalRows = queryData.totalRows
const pageSize = pagination.value.pageSize
const hasResults = queryData?.results?.length > 0
return calcPageCount(totalRows, pageSize, hasResults)
})
return {
// 供 ServerSideDataTable 可用的表格状态
pagination,
sorting,
columnFilters,
pageCount,
// TanStack Query 响应
...query,
}
}

就能把大多服务器表单需要的逻辑包装起来,灌入 ServerSideDataTable.vue

const customSearchs = ref({
foo: 'abc'
bar: '123'
})
const {
data: thingsData,
isFetching: isThingsFetching,
pageCount: thingsPageCount,
pagination: thingsPagination,
sorting: thingsSorting,
columnFilters: thingsColumnFilters,
refetch: thingsRefetch,
} = useServerSideDataTable<Things>({
queryKey: ['things', customSearchs],
queryFn: async ({ internalTableFilter, }) => {
const requestParams = {
// 已经构建好的 TableFilter,组合最终请求
...internalTableFilter.value,
...customSearchs
} satisfies ThingsFilter
return await fetchThings(requestParams) ;
},
})
<ServerSideDataTable
:columns="thingsColumns"
:data="thingsData"
:is-loading="isThingsFetching"
:page-count="thingsPageCount"
v-model:pagination="thingsPagination"
v-model:sorting="thingsSorting"
v-model:column-filters="thingsColumnFilters"/>

总结

对于 TanStack Table 这类大型成熟的套件 AI 有较多的知识储备,很多问题其实询问 AI 起个头再翻下文档都会明朗很多。

我很满意现在创建数据表单超轻松,且后续维护和扩充都非常方便!