Xây dựng Data Grid hiệu năng cao trong React với TanStack Table v8

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Thách thức khi xử lý tập dữ liệu lớn trong React

Hầu hết các nhà phát triển frontend sớm muộn gì cũng gặp khó khăn với các bảng dữ liệu (data table). Khi bạn map qua khoảng 50 dòng, một hàm .map() đơn giản vẫn hoạt động hoàn hảo. Tuy nhiên, khi tập dữ liệu của bạn tăng lên 1.000 hoặc 10.000 bản ghi, phương pháp tiêu chuẩn sẽ thất bại. Trình duyệt bắt đầu bị khựng, các tương tác có cảm giác bị lag và DOM trở nên quá nặng để quản lý.

TanStack Table v8 là lựa chọn hàng đầu để giải quyết các nút thắt cổ chai về hiệu năng này. Đây là một thư viện “headless”. Điều này có nghĩa là nó quản lý logic, trạng thái và API cho bảng của bạn mà không ép buộc một giao diện người dùng (UI) cụ thể nào. Bạn giữ toàn quyền kiểm soát HTML và CSS của mình. Trong các môi trường thực tế (production), tôi đã thấy cách tiếp cận này giúp giao diện luôn mượt mà ngay cả khi xử lý các tập dữ liệu vượt quá 10.000 dòng.

Bước 1: Cài đặt và Thiết lập Dự án

Đầu tiên, hãy tải gói core của table. Vì chúng ta muốn xử lý lượng lớn dữ liệu một cách mượt mà, chúng ta cũng sẽ cài thêm gói virtualization từ cùng một hệ sinh thái.

npm install @tanstack/react-table @tanstack/react-virtual

Nếu bạn thích dùng yarn hoặc pnpm, hãy sử dụng các lệnh sau:

yarn add @tanstack/react-table @tanstack/react-virtual
# hoặc
pnpm add @tanstack/react-table @tanstack/react-virtual

Phiên bản 8 được xây dựng hoàn toàn bằng TypeScript. Điều này mang lại khả năng kiểm soát kiểu (type safety) tuyệt vời, giúp bạn phát hiện các lỗi tiềm ẩn trong quá trình phát triển thay vì đợi đến sau khi triển khai.

Bước 2: Cấu hình và Logic Cốt lõi

Điểm mạnh cốt lõi của TanStack Table v8 là tính mô-đun. Bạn chỉ import những tính năng thực sự cần thiết, giúp giữ cho kích thước bundle nhỏ gọn. Hãy cùng thiết lập cấu hình để xử lý việc sắp xếp (sorting) và lọc (filtering).

Định nghĩa Cột và Dữ liệu

Bắt đầu bằng việc định nghĩa cấu trúc dữ liệu của bạn. Chúng ta sử dụng createColumnHelper để giữ cho các hàm render cell luôn gọn gàng và an toàn về kiểu dữ liệu.

import { createColumnHelper } from '@tanstack/react-table';

type User = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  status: string;
};

const columnHelper = createColumnHelper<User>();

const columns = [
  columnHelper.accessor('id', {
    header: () => <span>ID</span>,
    cell: info => info.getValue(),
  }),
  columnHelper.accessor('firstName', {
    header: 'Tên',
  }),
  columnHelper.accessor('lastName', {
    header: 'Họ',
  }),
  columnHelper.accessor('email', {
    header: 'Email',
  }),
  columnHelper.accessor('age', {
    header: 'Tuổi',
  }),
];

Triển khai Table Hook

Hãy coi hook useReactTable như bộ não cho component của bạn. Bạn sẽ kết nối các model sắp xếp và lọc tại đây. Vì thư viện có tính mô-đun, bạn phải cung cấp rõ ràng các row model mà bạn dự định sử dụng.

import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  SortingState,
} from '@tanstack/react-table';
import { useState } from 'react';

function DataTable({ data }) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [globalFilter, setGlobalFilter] = useState('');

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      globalFilter,
    },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  });

  return (
    <div className="p-4">
      <input
        value={globalFilter ?? ''}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Tìm kiếm tất cả các cột..."
        className="mb-4 p-2 border rounded"
      />
      <table>
        {/* Header và các dòng sẽ nằm ở đây */}
      </table>
    </div>
  );
}

Tăng tốc độ với Virtualization

Việc render 5.000 dòng cùng lúc sẽ tạo ra 5.000 phần tử <tr>. Một bảng có 15 cột sẽ dẫn đến 75.000 DOM node, điều này có thể làm treo trình duyệt di động. Virtualization giải quyết vấn đề này bằng cách chỉ render các dòng hiện đang hiển thị trong viewport (khung nhìn).

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

const tableContainerRef = useRef<HTMLDivElement>(null);
const { rows } = table.getRowModel();

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 35, // Chiều cao trung bình của một dòng tính bằng pixel
  overscan: 10, // Render trước 10 dòng để cuộn mượt mà hơn
});

const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();

Trong JSX, hãy áp dụng vị trí tuyệt đối (absolute position) cho các dòng bằng cách sử dụng giá trị start từ virtualizer. Điều này đánh lừa trình duyệt hiển thị một vùng cuộn lớn trong khi DOM vẫn rất nhẹ.

Bước 3: Xác minh và Giám sát

Sau khi thiết lập virtualization, bạn nên xác nhận xem hiệu quả đạt được có thực tế hay không. Tôi thường tuân theo một danh sách kiểm tra đơn giản để đảm bảo việc triển khai hiệu quả.

Kiểm tra số lượng DOM Node

Mở Chrome DevTools và xem tab Elements. Cuộn qua danh sách của bạn. Nếu bạn có 10.000 mục nhưng chỉ thấy khoảng 30 phần tử <tr> trong DOM cùng một lúc, thì virtualization của bạn đang hoạt động. Điều này làm giảm đáng kể mức sử dụng bộ nhớ.

Giám sát việc Re-render Component

Sử dụng React Profiler để ghi lại một phiên làm việc trong khi lọc hoặc sắp xếp. Nếu các cell riêng lẻ re-render không cần thiết, hiệu năng của bạn sẽ bị ảnh hưởng. Để khắc phục, hãy định nghĩa các đối tượng cột bên ngoài component hoặc bọc chúng trong useMemo.

Tránh các sai lầm phổ biến

  • Thiếu CSS: Virtualization sẽ thất bại nếu container cha thiếu chiều cao cố định và thuộc tính overflow: auto.
  • Cột không ổn định: Việc định nghĩa các cột bên trong thân component mà không sử dụng memoization sẽ buộc bảng phải reset lại sau mỗi lần render.
  • Logic nặng: Hãy giữ cho các hàm accessor hoạt động nhanh. Chuyển các bước biến đổi dữ liệu phức tạp về backend hoặc xử lý chúng trước khi đưa vào trạng thái của bảng.

Bằng cách làm theo các bước này, bạn sẽ xây dựng được một data grid mang lại cảm giác phản hồi nhanh và chuyên nghiệp. Người dùng có thể cuộn qua các tập dữ liệu khổng lồ mà không lo giao diện bị treo. Thiết lập này cũng cung cấp một nền tảng vững chắc để thêm các tính năng như thay đổi kích thước cột (column resizing) hoặc nhóm dòng (row grouping) sau này.

Share: