Skip to content

Table (components/ui/table.tsx)

Overview

A comprehensive table component that provides sorting, filtering, pagination, row selection, and virtual scrolling capabilities for managing tabular data.

Features

  • Column sorting
  • Data filtering
  • Pagination
  • Row selection
  • Virtual scrolling
  • Fixed headers
  • Column resizing
  • Custom cell rendering
  • Row actions
  • Bulk actions
  • Loading states
  • Empty states
  • Error handling
  • Responsive design
  • Accessibility support
  • Keyboard navigation

Dependencies

import * as React from 'react'
import { ChevronUpDown, MoreVertical } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from './button'
import { Checkbox } from './checkbox'
import { Select, SelectItem } from './select'
import { Spinner } from './spinner'

Props

interface TableProps {
  columns: TableColumn[]
  rows: any[]
  showSelection?: boolean
  showPagination?: boolean
  defaultSort?: { column: string, direction: 'asc' | 'desc' }
  onSort?: (column: string, direction: 'asc' | 'desc') => void
  onSelect?: (selectedRows: string[]) => void
  onPageChange?: (page: number) => void
  loading?: boolean
  error?: string
  className?: string
}

Implementation

State Management

interface TableState {
  sortColumn: string | null
  sortDirection: 'asc' | 'desc'
  selectedRows: string[]
  page: number
  pageSize: number
  filters: Record<string, any>
  loading: boolean
  error: string | null
}

const [tableState, setTableState] = useState<TableState>({
  sortColumn: null,
  sortDirection: 'asc',
  selectedRows: [],
  page: 1,
  pageSize: 10,
  filters: {},
  loading: false,
  error: null
})

Methods

// Handles column sort
const handleSort = (column: string) => {
  setTableState(prev => ({
    ...prev,
    sortColumn: column,
    sortDirection: prev.sortColumn === column && prev.sortDirection === 'asc' ? 'desc' : 'asc'
  }))
}

// Handles row selection
const handleRowSelect = (rowId: string) => {
  setTableState(prev => ({
    ...prev,
    selectedRows: prev.selectedRows.includes(rowId)
      ? prev.selectedRows.filter(id => id !== rowId)
      : [...prev.selectedRows, rowId]
  }))
}

// Handles bulk selection
const handleBulkSelect = (checked: boolean) => {
  setTableState(prev => ({
    ...prev,
    selectedRows: checked ? rows.map(row => row.id) : []
  }))
}

// Handles pagination
const handlePageChange = (page: number) => {
  setTableState(prev => ({
    ...prev,
    page
  }))
}

// Handles filter change
const handleFilterChange = (column: string, value: any) => {
  setTableState(prev => ({
    ...prev,
    filters: {
      ...prev.filters,
      [column]: value
    },
    page: 1
  }))
}

Unique Functionality

Virtual Scrolling System

const VirtualTable = memo(({ rows, rowHeight = 40 }) => {
  const containerRef = useRef<HTMLDivElement>(null)
  const [visibleRows, setVisibleRows] = useState<typeof rows>([])
  const [scrollTop, setScrollTop] = useState(0)

  useEffect(() => {
    if (!containerRef.current) return

    const startIndex = Math.floor(scrollTop / rowHeight)
    const endIndex = Math.min(
      rows.length,
      Math.ceil((scrollTop + containerRef.current.clientHeight) / rowHeight)
    )

    setVisibleRows(rows.slice(startIndex, endIndex))
  }, [rows, scrollTop, rowHeight])

  const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(event.currentTarget.scrollTop)
  }, [])

  return (
    <div
      ref={containerRef}
      className="max-h-[500px] overflow-auto"
      onScroll={handleScroll}
    >
      <div style={{ height: rows.length * rowHeight }}>
        <div
          style={{
            transform: `translateY(${Math.floor(scrollTop / rowHeight) * rowHeight}px)`
          }}
        >
          {visibleRows.map(row => (
            <TableRow key={row.id} {...row} />
          ))}
        </div>
      </div>
    </div>
  )
})

HTML Structure

<div className="w-full overflow-auto">
  <table className="w-full caption-bottom text-sm">
    <thead className={cn(
      "bg-muted/50 sticky top-0",
      "[&_tr]:border-b"
    )}>
      <tr className="border-b transition-colors hover:bg-muted/50">
        {showSelection && (
          <th className="h-12 w-12 px-4">
            <Checkbox
              checked={tableState.selectedRows.length === rows.length}
              onCheckedChange={handleBulkSelect}
            />
          </th>
        )}
        {columns.map(column => (
          <th
            key={column.key}
            className={cn(
              "h-12 px-4 text-left align-middle font-medium text-muted-foreground",
              column.sortable && "cursor-pointer select-none",
              column.className
            )}
            onClick={() => column.sortable && handleSort(column.key)}
          >
            <div className="flex items-center gap-2">
              {column.header}
              {column.sortable && tableState.sortColumn === column.key && (
                <ChevronUpDown className="h-4 w-4" />
              )}
            </div>
          </th>
        ))}
      </tr>
    </thead>

    <tbody>
      {tableState.loading ? (
        <tr>
          <td
            colSpan={columns.length + (showSelection ? 1 : 0)}
            className="h-24 text-center"
          >
            <Spinner className="h-6 w-6" />
          </td>
        </tr>
      ) : rows.length === 0 ? (
        <tr>
          <td
            colSpan={columns.length + (showSelection ? 1 : 0)}
            className="h-24 text-center"
          >
            No results found
          </td>
        </tr>
      ) : (
        <VirtualTable rows={rows} />
      )}
    </tbody>
  </table>

  {showPagination && (
    <div className="flex items-center justify-between px-2 py-4">
      <div className="flex-1 text-sm text-muted-foreground">
        {tableState.selectedRows.length} of {rows.length} row(s) selected
      </div>

      <div className="flex items-center gap-6 lg:gap-8">
        <div className="flex items-center gap-2">
          <p className="text-sm font-medium">Rows per page</p>
          <Select
            value={String(tableState.pageSize)}
            onValueChange={value => setTableState(prev => ({
              ...prev,
              pageSize: Number(value)
            }))}
          >
            <SelectItem value="10">10</SelectItem>
            <SelectItem value="20">20</SelectItem>
            <SelectItem value="50">50</SelectItem>
          </Select>
        </div>

        <div className="flex items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(tableState.page - 1)}
            disabled={tableState.page === 1}
          >
            Previous
          </Button>
          <div className="text-sm font-medium">
            Page {tableState.page} of {Math.ceil(rows.length / tableState.pageSize)}
          </div>
          <Button
            variant="outline"
            size="sm"
            onClick={() => handlePageChange(tableState.page + 1)}
            disabled={tableState.page === Math.ceil(rows.length / tableState.pageSize)}
          >
            Next
          </Button>
        </div>
      </div>
    </div>
  )}
</div>

API Integration

No direct API routes used - this is a UI component for displaying and managing tabular data.

Components Used

  • Button
  • Checkbox
  • Select
  • SelectItem
  • Spinner

Notes

  1. Implement efficient virtual scrolling
  2. Handle sorting and filtering properly
  3. Maintain responsive layout
  4. Support keyboard navigation
  5. Clean up event listeners on unmount