DEV Community

Cover image for Tôi sẽ không muốn tạo Table bằng bất cứ cách khác ?
Phúc Nguyễn
Phúc Nguyễn

Posted on

Tôi sẽ không muốn tạo Table bằng bất cứ cách khác ?

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:

Image description

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)}</>
        },
    })),
]
Enter fullscreen mode Exit fullscreen mode

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

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 đến day30).

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ột activity).
  • 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)