Nỗi ám ảnh mang tên Data Table trong React
Làm việc với bảng dữ liệu (Data Table) luôn nằm trong top những task gây ức chế nhất cho frontend developer. Nếu chỉ cần hiển thị vài dòng tĩnh, thẻ <table> HTML thuần là xong. Tuy nhiên, khi yêu cầu bắt đầu nhảy sang: sắp xếp (sorting), lọc (filtering), hay phân trang (pagination), bạn sẽ thấy mình lún sâu vào một mớ hỗn độn của logic và state.
Mình từng tham gia refactor một dự án khoảng 50.000 dòng code. Ở đó, các bảng dữ liệu được chắp vá bằng đủ loại thư viện UI khác nhau. Bài học xương máu là: nếu chọn sai công cụ từ đầu, việc bảo trì sau này sẽ ngốn của bạn hàng tuần trời chỉ để sửa một cái bug hiển thị. TanStack Table v8 (trước đây là React Table) ra đời để giải quyết bài toán này với tư duy hoàn toàn khác.
Ba cách tiếp cận phổ biến khi xây dựng Table
Trước khi gõ dòng code đầu tiên, hãy cùng nhìn lại các phương pháp phổ biến để hiểu tại sao giới chuyên gia lại ưu tiên TanStack Table.
1. Dùng HTML Table thuần
- Ưu điểm: Siêu nhẹ, không tốn thêm byte nào cho bundle size.
- Nhược điểm: Bạn phải tự viết code cho mọi logic. Chỉ riêng phần Sorting cũng đủ khiến code dài dằng dặc và cực kỳ dễ lỗi.
2. Dùng Component từ UI Library (MUI, Ant Design, Chakra UI)
- Ưu điểm: Ăn liền. Copy-paste là có ngay giao diện bóng bẩy.
- Nhược điểm: Cực khó tùy biến sâu. Nếu designer yêu cầu một layout bảng “phá cách” không có trong template, bạn sẽ phải vất vả ghi đè CSS (CSS override) rất mệt mỏi.
3. Dùng Headless UI (TanStack Table v8)
- Ưu điểm: Đây là thư viện “không giao diện”. Nó chỉ lo phần não bộ (logic, state), còn việc hiển thị như thế nào là quyền của bạn. Bạn thích dùng Tailwind, CSS Modules hay thẻ
<div>? Tùy ý. - Nhược điểm: Tốn thêm chút thời gian thiết lập ban đầu (boilerplate code).
Tại sao TanStack Table v8 lại ‘đáng đồng tiền bát gạo’?
Thực tế, TanStack Table v8 chỉ nặng khoảng 14-15kb nhưng lại cực kỳ mạnh mẽ. Nó hỗ trợ TypeScript tận răng, giúp bạn bắt lỗi ngay khi mapping dữ liệu sai. Điểm ăn tiền nhất là khả năng tách biệt hoàn toàn giữa Logic và UI. Bạn có thể xây dựng một Design System riêng cho bảng mà không bị bó buộc bởi bất kỳ quy chuẩn nào.
Đừng bao giờ chọn thư viện UI chỉ vì nó đẹp sẵn. Nếu dự án của bạn có xu hướng thay đổi yêu cầu liên tục, việc dùng một thư viện cứng nhắc sẽ biến thành cơn ác mộng khi cần refactor.
Hướng dẫn triển khai TanStack Table v8 từ A-Z
Để bắt đầu, hãy cài đặt gói thư viện vào dự án:
npm install @tanstack/react-table
Bước 1: Định nghĩa cấu trúc Column
Trong TanStack Table, columns đóng vai trò như một bản thiết kế. Nó xác định dữ liệu lấy từ đâu và hiển thị ra sao.
import { createColumnHelper } from '@tanstack/react-table';
const data = [
{ id: 1, name: 'Nguyen Van A', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Tran Thi B', email: '[email protected]', role: 'Editor' },
];
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: info => info.getValue(),
}),
columnHelper.accessor('name', {
header: 'Họ và Tên',
cell: info => <span className="font-bold">{info.getValue()}</span>,
}),
columnHelper.accessor('email', {
header: 'Email',
}),
];
Bước 2: Khởi tạo Table Instance
Sử dụng hook useReactTable để điều khiển toàn bộ trạng thái của bảng.
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
function MyDataTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<table className="w-full border-collapse">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} className="border p-2 bg-gray-50">
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="border p-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Bước 3: Thêm tính năng Sorting (Sắp xếp)
Để kích hoạt Sorting, bạn cần thêm getSortedRowModel và quản lý state sorting thông qua useState của React.
const [sorting, setSorting] = useState([]);
const table = useReactTable({
data,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
Bước 4: Thêm tính năng Filtering (Tìm kiếm)
Tính năng lọc yêu cầu getFilteredRowModel. Bạn có thể tạo một ô input đơn giản để lọc nhanh dữ liệu trên toàn bộ các cột (Global Filter).
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
state: { sorting, globalFilter },
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
// ... config khác
});
Bước 5: Thêm tính năng Pagination (Phân trang)
Phân trang giúp trình duyệt không bị “lag” khi phải xử lý hàng nghìn dòng dữ liệu cùng lúc.
const table = useReactTable({
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageSize: 10 } },
// ... config khác
});
Kinh nghiệm thực chiến khi xử lý dữ liệu lớn
Khi làm việc với dữ liệu thực tế từ API, đừng bao giờ xử lý Sorting hay Pagination ở phía client nếu danh sách lên đến 10.000 dòng. Thay vào đó, hãy dùng Manual Pagination. TanStack Table có các cờ như manualPagination: true để bạn đồng bộ trạng thái bảng trực tiếp với server.
Một mẹo nhỏ mình thường dùng là kết hợp với React Query. Khi người dùng bấm chuyển trang, React Query sẽ tự động fetch dữ liệu mới. Cách làm này giúp ứng dụng mượt mà và chuyên nghiệp hơn hẳn.
Cuối cùng, hãy tối ưu hiệu suất bằng cách memoize các cột và dữ liệu. Việc bảng re-render liên tục mỗi khi người dùng gõ một ký tự vào ô search là điều tối kỵ trong tối ưu UI.
Hy vọng bài viết này giúp bạn nắm vững tư duy “Headless UI”. Đừng ngại thử sức với các tính năng nâng cao như Column Resizing hay Row Selection để nâng cấp trải nghiệm người dùng lên một tầm cao mới.

