Skip to content

Repository files navigation

# Enterprise Data Grid — TanStack Table + Virtual A production-grade, virtualized data table for huge datasets, built with **Next.js 16**, **TanStack Table v8**, **TanStack Virtual v3**, **TanStack Query v5**, **dnd-kit**, and **Tailwind CSS v4**. ## Features - **Bulk row selection** — header checkbox (select all loaded rows), per-row checkboxes, indeterminate state, CSV export of the current selection. - **Column drag-and-drop reordering** — grab the handle on any unpinned column header (via `dnd-kit`) to reorder columns live. - **Excel-like column resizing** — drag the right edge of any header. Uses TanStack Table's built-in resizing state. - **Column pinning** — click the pin icon on a header to snap that column to the left edge of the table (sticky, always visible while scrolling horizontally). Click again to unpin. - **Row virtualization** — only the ~15–20 visible rows are ever mounted in the DOM, no matter whether the table holds 1,000 or 1,000,000 rows (via `@tanstack/react-virtual`). - **Offset-based pagination + infinite fetch** — the API takes `offset`/`limit`; the client fetches pages of 100 rows as you scroll near the bottom, using TanStack Query's `useInfiniteQuery`. - **19 data columns** (+ the selection column = 20 total): employee ID, name, email, phone, department, job title, status, location, country, manager, salary, experience, performance score, projects completed, join date, last login, skills. - **Search + department filter**, applied server-side alongside pagination. - **100,000-row mock dataset**, deterministically generated and cached in memory (no DB needed) — see `lib/mock-data.ts`. ## Getting started ```bash npm install npm run dev ``` Open http://localhost:3000. To build for production: ```bash npm run build npm start ``` ## Trying out each requirement - **Bulk select**: click a few row checkboxes → an action bar appears with the selected count, a "Clear" button, and an "Export CSV" button that downloads exactly the rows you selected. - **Drag to reorder**: hover a column header (not the pinned checkbox column, not a currently-pinned column) — a grip icon (⋮⋮) fades in on the left. Drag it left/right to reorder. - **Resize**: hover the right edge of any header — the cursor changes to a column-resize cursor. Drag to resize; the column's minimum width prevents it from collapsing. - **Pin**: hover a header, click the pin icon on the right. The column immediately moves to the fixed left region of the table (after the checkbox column) and stays visible while you scroll horizontally. Click the pin icon again (now solid) to unpin it back into the scrollable area. - **Huge data**: scroll down — the footer text ("X of 100,000 rows loaded") climbs as more pages are fetched automatically when you approach the bottom of the current data. ## Architecture notes ### Why offset pagination + virtualization together The API (`app/api/employees/route.ts`) is a classic offset/limit endpoint — `?offset=0&limit=100` — that also accepts `search` and `department` query params and returns `{ rows, nextOffset, totalCount, filteredCount }`. This is the same shape you'd use for a real Postgres/Mongo-backed table (`OFFSET x LIMIT y`). On the client, `useInfiniteQuery` treats each `offset` as a "page" and keeps appending rows into one flat array as the user scrolls (`app/_components/data-table.tsx`). `useVirtualizer` then renders only the rows currently in the viewport (plus a small overscan buffer), so the DOM stays small even after tens of thousands of rows have been fetched. Fetching more data and rendering more DOM nodes are two independent concerns — this pattern keeps both cheap at any dataset size. ### Why the table isn't a real `` Pinned columns + virtualized rows need arbitrary sticky positioning that plain HTML tables don't support well. The table is built from `display:flex` divs (`role`-equivalent structure) — each header/cell gets an explicit pixel width from TanStack Table's column-sizing state, and pinned columns get `position: sticky; left: px` computed via `column.getStart('left')`. This is the same approach used in TanStack's own official pinning + virtualization examples. ### Where to scale things up - `TOTAL_ROWS` in `lib/mock-data.ts` controls the size of the mock dataset (currently 100,000). Bump it freely — the generation is deterministic (seeded per row) and cached in memory after the first request, so response times don't grow with repeated requests. - `FETCH_SIZE` in `app/_components/data-table.tsx` controls how many rows are fetched per page (currently 100). - `SIMULATED_LATENCY_MS` in the API route simulates realistic network latency; set to `0` for a real backend. ### Swapping in a real database Replace the body of `GET` in `app/api/employees/route.ts` with your real query, e.g.: ```ts const rows = await db.employee.findMany({ skip: offset, take: limit, where: { ... } }); const filteredCount = await db.employee.count({ where: { ... } }); ``` Nothing on the client needs to change — it only depends on the `{ rows, nextOffset, totalCount, filteredCount }` response shape. ## Stack - Next.js 16 (App Router, Turbopack) - TanStack Table v8 (core + column sizing/pinning/ordering/row-selection) - TanStack Virtual v3 (row virtualization) - TanStack Query v5 (`useInfiniteQuery` for paginated fetch) - dnd-kit (`core` + `sortable`) for column drag-and-drop - Tailwind CSS v4 - @faker-js/faker (deterministic mock data) # data-table

About

experimenting tanstack table with tanstack virtual

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages