Skip to main content
EJ Centeno

Building a Stock Administration System with React 19 and TanStack Table v8

January 28, 2026 · 6 min read

Building a Stock Administration System with React 19 and TanStack Table v8

One of my current projects at C3 Interactive Manila is a stock administration system — specifically an Equity Self Service Platform for a financial services client. The system manages ESPP (Employee Stock Purchase Plans), LTIP (Long-Term Incentive Plans), stock option grants, and related financial instruments. It's a data-heavy enterprise application where tables are everywhere.

TanStack Table v8 (formerly React Table) has been central to how we handle that data. Here's what building real features with it looks like.

Why TanStack Table v8

We evaluated a few options: AG Grid, MUI DataGrid, and TanStack Table. AG Grid is powerful but felt heavy and brought a lot of its own opinions. MUI DataGrid is excellent if you're already on Material UI, but we're using shadcn/ui with Tailwind. TanStack Table is headless — it handles the logic, you handle the UI. That composability with shadcn/ui was the deciding factor.

The Core Setup

The basic pattern in TanStack Table v8 is defining column definitions, creating a table instance with useReactTable, and then rendering the table structure yourself.

You define a ColumnDef array where each entry specifies an accessorKey (or accessorFn), a header label, and optionally a custom cell renderer. The table instance is created by calling useReactTable with your data, columns, and the row model functions you want to enable — getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel.

The ColumnDef type is the key interface you'll work with constantly. We define TypeScript interfaces for each data entity and let the column definitions be fully typed against them. If the API changes and a field is renamed, TypeScript catches every column definition that references the old name.

Server-Side Pagination and Sorting

For our ESPP participant tables, we have thousands of rows. Client-side pagination wasn't viable. TanStack Table supports manual (server-side) pagination by setting manualPagination: true and providing the pageCount from your API response.

We pair this with TanStack Query. The query key includes the current page index, page size, and sort state, so every time the user changes the sort order or navigates pages, a new query fires automatically. When the user clicks a column header to sort, we update the sorting state, which updates the query key, which triggers a new fetch. It's clean and predictable.

One pattern that took us a while to get right: we keep both the pagination and sorting state in the component with useState, pass them to useReactTable via the state option, and use onPaginationChange and onSortingChange to update them. This controlled state pattern also means we can sync pagination to URL search params for shareable links.

shadcn/ui DataTable Composition

The shadcn/ui DataTable pattern is a wrapper component that takes your TanStack table instance and renders the table, thead, tbody, and tr elements using the table's row model API. We extended this pattern to support sticky headers using position sticky on the thead, column pinning for identifier columns, and custom cell renderers for status badges and action buttons.

The composability is real. Adding a row selection column is just adding a ColumnDef with a checkbox cell and header. Adding expandable rows means adding getExpandedRowModel to the table options and a toggle cell. You're building with primitives, not fighting a component API.

Controlled Pagination Gotchas

Reset pagination to page 0 when filters change. This sounds obvious but it's easy to forget when you're wiring up filter inputs. If a user is on page 5 and applies a filter that returns only 20 records, they'll see an empty table. We handle this explicitly in all filter change handlers.

Also: manualPagination: true means TanStack Table won't try to slice your data array. It trusts that the data you provide is already the correct page. Make sure your API returns only the rows for the current page, not all rows.

TypeScript Interfaces

We define strict TypeScript interfaces for every entity in the system — EsppGrant, LtipGrant, StockOption, ParticipantAccount. Column definitions are typed against these interfaces, so mismatched field names are compile-time errors. Combined with TanStack Table's generics, the entire table layer is type-safe end to end.

What I'd Do Differently

Column definition organization. We started with column definitions inline in the page component. As tables grew complex, those files got unwieldy. We've since moved to co-located columns.tsx files that live next to the page component. That separation makes the code much easier to navigate.

TanStack Table v8 is verbose compared to a turnkey solution, but the control you get in return is worth it for an enterprise application with complex, client-specific requirements.

← Back to all posts