Interactive Tables (DataTables/TanStack Table) Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Interactive Tables (DataTables/TanStack Table) on Website

A table with sorting and pagination is one of the most common frontend tasks, and one of the most often implemented poorly. jQuery DataTables is still found in legacy projects, but in modern React applications, TanStack Table (formerly react-table) has become the de facto standard — a headless library without built-in styles that gives complete control over markup.

Library Selection

TanStack Table v8 — for React/Vue/Solid/Svelte projects. Headless: no styles included, just logic. Size ~14 KB gzipped.

jQuery DataTables — justified only if site already uses jQuery and there's no reason to add React for one table. Otherwise — avoid.

AG Grid Community — when you need 100,000+ row virtualization, Excel export, and cell editing. Heavier, but more powerful.

TanStack Table: Basic Implementation

npm install @tanstack/react-table

Define columns and connect hook:

import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  getPaginationRowModel,
  getFilteredRowModel,
  useReactTable,
  SortingState,
} from '@tanstack/react-table'

type Order = {
  id: string
  customer: string
  amount: number
  status: 'pending' | 'paid' | 'cancelled'
  createdAt: string
}

const columnHelper = createColumnHelper<Order>()

const columns = [
  columnHelper.accessor('id', {
    header: 'Order #',
    cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
  }),
  columnHelper.accessor('customer', {
    header: 'Customer',
    enableSorting: true,
  }),
  columnHelper.accessor('amount', {
    header: 'Amount',
    cell: (info) => `${info.getValue().toLocaleString('en-US')} $`,
    sortingFn: 'basic',
  }),
  columnHelper.accessor('status', {
    header: 'Status',
    cell: (info) => <StatusBadge status={info.getValue()} />,
    enableSorting: false,
  }),
  columnHelper.accessor('createdAt', {
    header: 'Date',
    sortingFn: 'datetime',
  }),
]
function OrdersTable({ data }: { data: Order[] }) {
  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(),
    getPaginationRowModel: getPaginationRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    initialState: { pagination: { pageSize: 25 } },
  })

  return (
    <div>
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Search all fields..."
        className="mb-4 w-64 border rounded px-3 py-2"
      />
      <table className="w-full border-collapse">
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  onClick={header.column.getToggleSortingHandler()}
                  className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
                >
                  {flexRender(header.column.columnDef.header, header.getContext())}
                  {{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? ''}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id} className="hover:bg-gray-50">
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <div className="flex items-center gap-2 mt-4">
        <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
          ←
        </button>
        <span>
          Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
        </span>
        <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
          →
        </button>
        <select
          value={table.getState().pagination.pageSize}
          onChange={(e) => table.setPageSize(Number(e.target.value))}
        >
          {[10, 25, 50, 100].map((size) => (
            <option key={size} value={size}>per {size}</option>
          ))}
        </select>
      </div>
    </div>
  )
}

Server-Side Pagination

For large datasets (10,000+ rows), client-side pagination doesn't work — everything must go through API:

const [{ pageIndex, pageSize }, setPagination] = useState({
  pageIndex: 0,
  pageSize: 25,
})

// React Query for data loading
const { data, isFetching } = useQuery({
  queryKey: ['orders', pageIndex, pageSize, sorting, globalFilter],
  queryFn: () =>
    fetchOrders({
      page: pageIndex + 1,
      limit: pageSize,
      sortBy: sorting[0]?.id,
      sortDir: sorting[0]?.desc ? 'desc' : 'asc',
      search: globalFilter,
    }),
  keepPreviousData: true, // doesn't flicker when switching pages
})

const table = useReactTable({
  data: data?.rows ?? [],
  columns,
  pageCount: data?.pageCount ?? -1,
  state: { sorting, pagination: { pageIndex, pageSize }, globalFilter },
  manualPagination: true,  // key flag
  manualSorting: true,
  manualFiltering: true,
  onPaginationChange: setPagination,
  // ...
})

Export to CSV

function exportToCSV(table: Table<Order>) {
  const headers = table.getAllColumns()
    .filter((col) => col.getIsVisible())
    .map((col) => col.columnDef.header as string)

  const rows = table.getFilteredRowModel().rows.map((row) =>
    row.getVisibleCells().map((cell) => {
      const value = cell.getValue()
      return typeof value === 'string' && value.includes(',') ? `"${value}"` : value
    })
  )

  const csv = [headers, ...rows].map((r) => r.join(',')).join('\n')
  const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `orders-${Date.now()}.csv`
  a.click()
  URL.revokeObjectURL(url)
}

\uFEFF — BOM for proper Cyrillic display in Excel.

Row Virtualization

For tables with thousands of rows on client — TanStack Virtual:

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual'

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

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 48, // row height in px
  overscan: 10,
})

// In JSX:
<div ref={tableContainerRef} style={{ height: '600px', overflow: 'auto' }}>
  <table>
    <tbody style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
      {rowVirtualizer.getVirtualItems().map((virtualRow) => {
        const row = rows[virtualRow.index]
        return (
          <tr
            key={row.id}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: `${virtualRow.size}px`,
            }}
          >
            {row.getVisibleCells().map((cell) => (
              <td key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        )
      })}
    </tbody>
  </table>
</div>

10,000 rows render in ~20 ms — only what's visible in viewport is in DOM.

What We Do

Analyze data structure and requirements: volume, update frequency, need inline-edit, export, fixed columns. Choose client-side or server-side pagination model for the task, configure columns, sorting, filtering. Style according to project design system — table looks like part of the interface, not a widget from another app.

Timeframe: basic table with sorting and pagination — 1 day. With server-side pagination, filters, and export — 2–3 days.