perf(data-table): cache pinned column class resolution

- reuse the pinned column lookup while table props stay stable to reduce repeated per-render work.
- share the resolved column class handler across unified and split-header table layouts.
- localize page-number screen reader labels so pagination remains accessible in every locale.
This commit is contained in:
QuentinHsu
2026-06-10 14:40:42 +08:00
parent 9691ca06d1
commit 40d0d6a82f
9 changed files with 71 additions and 22 deletions
@@ -23,17 +23,19 @@ export function getResolvedColumnClassName(
getColumnClassName?: DataTableColumnClassName,
pinnedColumns?: DataTablePinnedColumn[]
): DataTableColumnClassName {
if (!pinnedColumns?.length) {
return (columnId, kind) => getColumnClassName?.(columnId, kind)
}
const pinnedColumnById = new Map(
pinnedColumns.map((column) => [column.columnId, column])
return getResolvedColumnClassNameFromMap(
getColumnClassName,
getPinnedColumnMap(pinnedColumns)
)
}
export function getResolvedColumnClassNameFromMap(
getColumnClassName?: DataTableColumnClassName,
pinnedColumnById?: Map<string, DataTablePinnedColumn>
): DataTableColumnClassName {
return (columnId, kind) => {
const pinnedColumn = pinnedColumnById.get(columnId)
const customClassName = getColumnClassName?.(columnId, kind)
const pinnedColumn = pinnedColumnById?.get(columnId)
if (!pinnedColumn) return customClassName
@@ -41,6 +43,12 @@ export function getResolvedColumnClassName(
}
}
export function getPinnedColumnMap(pinnedColumns?: DataTablePinnedColumn[]) {
if (!pinnedColumns?.length) return undefined
return new Map(pinnedColumns.map((column) => [column.columnId, column]))
}
function getPinnedColumnClassName(
pinnedColumn: DataTablePinnedColumn,
kind: 'header' | 'cell'
@@ -20,14 +20,21 @@ import * as React from 'react'
import { type Row } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
import { getResolvedColumnClassName } from './column-pinning'
import {
getPinnedColumnMap,
getResolvedColumnClassNameFromMap,
} from './column-pinning'
import { DataTableColgroup } from './data-table-colgroup'
import { DataTableHeader } from './data-table-header'
import { DataTableRow } from './data-table-row'
import { TableEmpty } from './table-empty'
import { getTableSizeStyle } from './table-sizing'
import { TableSkeleton } from './table-skeleton'
import type { DataTableColumnClassName, DataTableViewProps } from './types'
import type {
DataTableColumnClassName,
DataTablePinnedColumn,
DataTableViewProps,
} from './types'
export type {
DataTableColumnClassName,
@@ -40,6 +47,10 @@ export { DataTableRow } from './data-table-row'
export function DataTableView<TData>(props: DataTableViewProps<TData>) {
const rows = props.rows ?? props.table.getRowModel().rows
const colSpan = props.table.getVisibleLeafColumns().length
const columnClassName = useResolvedColumnClassName(
props.getColumnClassName,
props.pinnedColumns
)
return (
<div
@@ -50,9 +61,19 @@ export function DataTableView<TData>(props: DataTableViewProps<TData>) {
{...props.containerProps}
>
{props.splitHeader ? (
<SplitHeaderTableView props={props} rows={rows} colSpan={colSpan} />
<SplitHeaderTableView
props={props}
rows={rows}
colSpan={colSpan}
getColumnClassName={columnClassName}
/>
) : (
<UnifiedTableView props={props} rows={rows} colSpan={colSpan} />
<UnifiedTableView
props={props}
rows={rows}
colSpan={colSpan}
getColumnClassName={columnClassName}
/>
)}
</div>
)
@@ -62,15 +83,13 @@ function UnifiedTableView<TData>({
props,
rows,
colSpan,
getColumnClassName,
}: {
props: DataTableViewProps<TData>
rows: Row<TData>[]
colSpan: number
getColumnClassName: DataTableColumnClassName
}) {
const getColumnClassName = getResolvedColumnClassName(
props.getColumnClassName,
props.pinnedColumns
)
const tableSizing = getTableSizing(props)
return (
@@ -94,17 +113,15 @@ function SplitHeaderTableView<TData>({
props,
rows,
colSpan,
getColumnClassName,
}: {
props: DataTableViewProps<TData>
rows: Row<TData>[]
colSpan: number
getColumnClassName: DataTableColumnClassName
}) {
const headerHostRef = React.useRef<HTMLDivElement>(null)
const bodyHostRef = React.useRef<HTMLDivElement>(null)
const getColumnClassName = getResolvedColumnClassName(
props.getColumnClassName,
props.pinnedColumns
)
const tableSizing = getTableSizing(props)
React.useEffect(() => {
@@ -174,6 +191,22 @@ function SplitHeaderTableView<TData>({
)
}
function useResolvedColumnClassName(
getColumnClassName?: DataTableColumnClassName,
pinnedColumns?: DataTablePinnedColumn[]
) {
const pinnedColumnById = React.useMemo(
() => getPinnedColumnMap(pinnedColumns),
[pinnedColumns]
)
return React.useMemo(
() =>
getResolvedColumnClassNameFromMap(getColumnClassName, pinnedColumnById),
[getColumnClassName, pinnedColumnById]
)
}
function getTableSizing<TData>(props: DataTableViewProps<TData>): {
colgroup?: React.ReactNode
style?: React.CSSProperties
+3 -1
View File
@@ -134,7 +134,9 @@ export function DataTablePagination<TData>({
)}
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
>
<span className='sr-only'>Go to page {pageNumber}</span>
<span className='sr-only'>
{t('Go to page {{page}}', { page: pageNumber })}
</span>
{pageNumber}
</Button>
)}
+1
View File
@@ -1856,6 +1856,7 @@
"Go to io.net API Keys": "Go to io.net API Keys",
"Go to last page": "Go to last page",
"Go to next page": "Go to next page",
"Go to page {{page}}": "Go to page {{page}}",
"Go to previous page": "Go to previous page",
"Go to settings": "Go to settings",
"Go to Settings": "Go to Settings",
+1
View File
@@ -1856,6 +1856,7 @@
"Go to io.net API Keys": "Accéder aux clés API io.net",
"Go to last page": "Aller à la dernière page",
"Go to next page": "Aller à la page suivante",
"Go to page {{page}}": "Aller à la page {{page}}",
"Go to previous page": "Aller à la page précédente",
"Go to settings": "Aller aux paramètres",
"Go to Settings": "Aller aux paramètres",
+1
View File
@@ -1856,6 +1856,7 @@
"Go to io.net API Keys": "io.net API キーへ移動",
"Go to last page": "最後のページへ移動",
"Go to next page": "次のページへ移動",
"Go to page {{page}}": "{{page}}ページ目へ移動",
"Go to previous page": "前のページへ移動",
"Go to settings": "設定へ",
"Go to Settings": "設定へ移動",
+1
View File
@@ -1856,6 +1856,7 @@
"Go to io.net API Keys": "Перейти к ключам API io.net",
"Go to last page": "Перейти на последнюю страницу",
"Go to next page": "Перейти на следующую страницу",
"Go to page {{page}}": "Перейти на страницу {{page}}",
"Go to previous page": "Перейти на предыдущую страницу",
"Go to settings": "Перейти к настройкам",
"Go to Settings": "Перейти к настройкам",
+3 -2
View File
@@ -1851,11 +1851,12 @@
"Go Back": "Quay lại",
"Go back and edit": "Quay lại và chỉnh sửa",
"Go to Dashboard": "Truy cập Dashboard",
"Go to first page": "Go to the first page",
"Go to first page": "Đi đến trang đầu tiên",
"Go to home": "Về trang chủ",
"Go to io.net API Keys": "Đi đến Khóa API io.net",
"Go to last page": "Go to the last page",
"Go to last page": "Đi đến trang cuối cùng",
"Go to next page": "Đi đến trang tiếp theo",
"Go to page {{page}}": "Đi đến trang {{page}}",
"Go to previous page": "Quay lại trang trước",
"Go to settings": "Đi tới cài đặt",
"Go to Settings": "Đi đến Cài đặt",
+1
View File
@@ -1856,6 +1856,7 @@
"Go to io.net API Keys": "前往 io.net API 密钥",
"Go to last page": "前往末页",
"Go to next page": "前往下一页",
"Go to page {{page}}": "前往第 {{page}} 页",
"Go to previous page": "前往上一页",
"Go to settings": "前往设置",
"Go to Settings": "前往设置",