Xin chào mọi người, hôm nay lại là một bài viết về một lib quen thuộc mà mình hay sử dụng trong khi lập trình frontend.
Có thể nói rằng Form và Table là hai thành phần con cực kỳ quan trọng và cũng không kém phần phức tạp trong lập trình giao diện Web. Với React JS chúng ta đã quá quen thuộc với ReactHookForm, vậy còn với Table chúng ta sẽ sử dụng lib nào ?
Nếu như bạn đã từng nghe đến Tanstack chắc hẳn bạn sẽ từng nghe đến bộ kit open source chất lượng cao từ nhà phát triển này. Tanstack Table là một trong số đó.
- Tanstack Table là một Headless UI cho việc xây dựng và phát triển table một cách mạnh mẽ. Chính vì là Headless UI nên chúng vô cùng linh hoạt và nhẹ. Không có những thành phần UI cứng nhắc, thay vào đó chúng giúp ta xử lý các logic cốt lõi và thoả sức sáng tạo với giao diện mà chúng ta mong muốn. Đó cũng chính là lý do vì sao mà SHADCN UI khuyến khích user dùng Component Table của họ cùng với Tanstack Table.
- Lợi ích thứ 2 phải kể đến việc chúng đem lại hiệu suất làm việc rất cao.Thử tưởng tượng một bảng table của bạn có hàng ngàn bản ghi như excel, vậy cây DOM sẽ mất bao lâu để có thể load toàn bộ các cột hàng trong bảng. Với cơ chế ảo hoá được tích hợp kèm react-vitualization chúng ta có thể hiển thị dữ liệu lớn mà không lo ứng dụng bị chậm.
- Lợi ích thứ 3 đó chính là các chức năng phụ hỗ trợ đi kèm như: Sắp xếp, Phân trang, Lọc dữ liệu, Nhóm cột, Kéo thả. Thông thường các chức năng này nếu xử lý bằng mã thông thường, lập trình viên frontend phải mất hàng giờ để có thể hoàn tất.
Sự hỗ trợ đa dạng ngôn ngữ cùng framework chính là lợi thế lớn nhất của Tanstack Table.
Với một ví dụ đơn giản sau đây tôi có thể chỉ cho bạn cách làm quen và sử dụng với framework này.
Tôi sẽ bỏ qua bước cài đặt môi trường vì nó khá đơn giản và quen thuộc với các lập trình viên.
Giả dụ tôi đang có một bảng thống kê các hoạt động.
Chúng ta sẽ chia cấu trúc thư mục như sau:
File Column.tsx
export const columnsOverview: ColumnDef<any>[] = [
{
accessorKey: 'activity',
header: '',
cell: (info: any) => {
const value = JSON.parse(info.getValue())
return (
<div className="flex items-end gap-2">
{getActionIcon(value.type)}
<div className="bg-gradient-to-r from-red-500 via-orange-300 to-yellow-200 size-[30px] border border-white p-2 rounded-full flex items-center justify-center">
<p className="text-[12px] text-white">{value.total}</p>
</div>
</div>
)
},
},
...Array.from({ length: 30 }, (_, i) => ({
accessorKey: `day${i + 1}`,
header: `${i + 1}`,
cell: (info: any) => {
const value = info.getValue()
if (value === 0) return <div className={cn('size-[100px]')}></div>
return <>{getChainFire(value)}</>
},
})),
]
File TableOverview.tsx
export default function TableOverview<TData, TValue>({ columns, month, filter }: TableOverviewProps<TData, TValue>) {
const [data, setData] = useState<TData[]>([])
const parentRef = useRef(null)
useEffect(() => {
const data = getChainFireData(1, month, filter, 30)
setData(transformChainData(data, month) as TData[])
}, [])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columns.length - 1,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
overscan: 5,
})
return (
<div>
<div className="flex">
<div>
<Table>
<TableHeader className="">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="h-[100px] border-none">
<TableHead></TableHead>
</TableRow>
))}
</TableHeader>
<TableBody className=" border-r border-[#4d86ab] ">
{table.getRowModel().rows.map((row, index) => (
<TableRow key={row.id} className="h-[100px] border-t-0">
<TableCell
className={cn(
'px-6 text-sm text-gray-700',
index < table.getRowModel().rows.length - 1
? 'border-b border-[#4d86ab]'
: ''
)}
>
{flexRender(
row.getVisibleCells()[0].column.columnDef.cell,
row.getVisibleCells()[0].getContext()
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div ref={parentRef} className="overflow-x-auto overflow-y-hidden scroll-ct w-full">
<div
style={{
width: `${columnVirtualizer.getTotalSize()}px`,
height: 'auto',
position: 'relative',
}}
>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="h-[100px] border-none">
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const header = headerGroup.headers[virtualColumn.index + 1]
return (
<TableHead
key={header.id}
style={{
transform: `translateX(${virtualColumn.start}px)`,
width: `${virtualColumn.size}px`,
position: 'absolute',
}}
className="p-0 text-left text-sm font-semibold text-gray-600 justify-center"
>
{header.column.columnDef.header !== '' && (
<div className="flex items-center justify-center flex-col gap-1">
<div className="text-[20px] font-normal text-[#4d86ab]">
{getDayOfWeek(
parseInt(
header.column.columnDef.header?.toString() ??
'12'
),
parseInt(month),
today.getFullYear()
)}
</div>
<div
className={cn(
'size-[60px] rounded-full bg-[#cb8989] items-center justify-center flex text-[30px] font-normal text-white'
)}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
</div>
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[100px] border-b border-[#4d86ab]">
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const cell = row.getVisibleCells()[virtualColumn.index + 1]
return (
<TableCell
className="p-0 w-[100px]"
key={cell.id}
onClick={() => console.log('Clicky')}
style={{
transform: `translate(${virtualColumn.start}px, ${0 * 90}px)`,
width: `${virtualColumn.size}px`,
position: 'absolute',
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="px-4 py-4 text-center text-sm text-gray-500"
>
No data available
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</div>
)
}
Giải thích cho hai file code trên
Định nghĩa cột trong bảng
- Cột đầu tiên: Là cột tiêu đề, không hiển thị dữ liệu mà chứa thông tin tiêu đề.
-
Các cột còn lại: Hiển thị dữ liệu trong 30 ngày (từ
day1
đếnday30
).
Các thành phần chính:
-
accessorKey
: Xác định dữ liệu từactivity
trong mảng dữ liệu. -
cell
: Render nội dung của ô cột, bao gồm biểu tượng (icon) và số liệu tổng (value.total
). -
JSON.parse
: Giải mã chuỗi JSON để hiển thị dữ liệu chi tiết. -
getActionIcon(value.type)
: Hàm tạo biểu tượng phù hợp với loại hoạt động.
Xây dựng bảng và truyền dữ liệu
-
useReactTable
: Khởi tạo bảng từ dữ liệu và cột. -
getCoreRowModel
: Quản lý mô hình hàng (row model
).
Ảo hóa cột với useVirtualizer
-
horizontal: true
: Ảo hóa cột theo chiều ngang. -
count
: Số lượng cột (trừ cộtactivity
). -
estimateSize
: Kích thước cột khoảng 100px. -
overscan
: Render thêm 5 cột ngoài vùng nhìn thấy để tránh lag khi cuộn.
Đây chỉ là một ví dụ đơn giản trong việc tạo bảng với tanstack table. Trong thực tế chúng ta còn có thể kết hợp rất nhiều các kỹ thuật khác nhau như đã liệt kê tại phần lợi ích bên trên. Chúng ta có thể sáng tạo rất nhiều trên lib này, nó là lib mà mình rất yêu thích. Chúc tất cả các bạn thành công.
Top comments (0)