This article also exists in: Italian

In the development of enterprise dashboards, managing data tables is often one of the most complex challenges. In one of my test projects, the GridContainer.tsx and Grid.tsx components represent an excellent example of how to leverage the power of React Table (v7) to create a flexible, powerful, and highly customizable interface.

The “Headless” Approach

The peculiarity of react-table is its being a headless library. It doesn’t provide UI components (no pre-packaged <table> tags), but it provides hooks that manage all the state logic: sorting, pagination, expansion, and selection.

In the test, I tried to use an architecture with the Container/Presenter pattern:

  • GridContainer: The “Brain”. Decides which columns to show, how to format links, and whether to activate pagination.
  • Grid: The “Muscle”. Takes the instructions and generates the HTML tags, manages the CSS, and ensures that the user physically sees the sorting icons or expanded rows.

GridContainer Component

In GridContainer.tsx, we use the main useTable hook combining it with several plugins:

const gridConfig = useTable(
    gridData,
    useSortBy,    // Manages column sorting
    useExpanded,  // Manages row expansion
    !!withPagination && usePagination // Manages pagination (optional)
);

This modularity allows loading only the necessary code, keeping the component lightweight.

The “Trick” of Column Pre-processing

One of the most interesting features of our GridContainer is how it transforms a “flat” data definition into a rich UI. Before passing the columns to react-table, the component performs a dynamic mapping:

tableData.columns = tableData.columns.map((el) => {
    // 1. Dynamic expansion injection
    if (!!subRow && !!el.NeedCellExpand) {
        el.Cell = ({ row }) => (
            <span {...row.getToggleRowExpandedProps()}>
                {row.isExpanded ? '👇' : '👉'}
            </span>
        );
    }
    // 2. Conditional rendering of links and badges
    if (!!el.NeedCellLink) {
        el.Cell = ({ row }) => (
            <ButtonLink link={...} textButton={row.original[el.NeedCellLink]} />
        );
    }
    return el;
});

Particularities: Row Expansion & Custom Cells

  • Row Expansion: Thanks to useExpanded, we can make rows interactive. The Grid.tsx component receives a renderSubElement function that allows showing additional details under the main row without cluttering the parent table structure.
  • Polymorphic Cells: Instead of just showing text, the component transforms data into ButtonLink, Badge, or icons based on specific flags like NeedMultipleLink or NeedCellLink.

Evolution: Towards TanStack Table v8

The React world moves fast, and react-table has evolved into TanStack Table v8. If we were to update our component today, here are the main differences we would face:

  1. TypeScript First: While v7 uses plugins as hook arguments, v8 was completely rewritten in TS. Column definition would become more structured through a ColumnHelper.
  2. State Management: In v8, state is more explicit. Plugins are no longer passed as hooks, but “features” are enabled directly in the useReactTable configuration object.
  3. Extreme Modularity: v8 is even lighter and completely removes the “plugin” concept in favor of a declarative configuration.

Refactoring Example (v7 vs v8)

Today (v7):

useTable({ columns, data }, useSortBy, usePagination)

Tomorrow (v8):

useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(), // Explicit sorting feature
  getPaginationRowModel: getPaginationRowModel(), // Pagination feature
})

Grid Component

The Grid.tsx component acts as the Presentation Layer (or visualization layer) of the system. While GridContainer handles business logic and plugin configuration, Grid has the task of transforming the data processed by react-table into actual HTML code.

Here is what it specifically handles:

1. Rendering the <table> HTML Structure

This is where the “marriage” between headless logic and the DOM happens. The component applies the so-called prop getters provided by react-table to ensure the table is accessible and correctly structured:

<table {...getTableProps()} className={css['grid--bordered']}>
    <thead>{/* ... */}</thead>
    <tbody {...getTableBodyProps()}>{/* ... */}</tbody>
</table>

2. Sorting Interface Management

Grid.tsx takes care of visually showing the sorting state in column headers, adding visual indicators (arrows) based on the isSorted and isSortedDesc state:

<span>
    {column.isSorted
        ? column.isSortedDesc
            ? ' 🔽'
            : ' 🔼'
        : ''}
</span>

3. Row Life Cycle (prepareRow)

A critical task is executing prepareRow(row). In react-table, rows are lazy-loaded; Grid prepares each row just before rendering, calculating the styles and properties needed for the cells.

4. Rendering Sub-Rows (Expanded Rows)

The component implements visualization logic for additional details. If a row is expandable and the isExpanded state is active, Grid.tsx injects an extra row into the <tbody> to render the renderSubElement:

{row.isExpanded ? (
    <tr>
        <td colSpan={visibleColumns.length}>
            {renderSubElement({ row, ...otherInfo })}
        </td>
    </tr>
) : null}

5. Debugging and Transparency

It includes a debug mode that allows viewing the internal JSON state of the expansion, very useful during development to understand how the react-table engine is reacting to user interactions.

Visual Analysis

Here is a description of the data flow from the source to the final rendering in the DOM, highlighting the central role of the TanStack react-table dependency.

Visual Analysis

1. The Transformer (GridContainer) GridContainer.tsx receives raw data. Even before the library comes into play, it performs column “enrichment”. If a column has the NeedCellLink flag, a Cell function is injected that renders a React component (ButtonLink). This is a powerful pattern: data decides its appearance.

2. The Engine (react-table) In GridElementContainer, we call the react-table hooks. This is where the magic happens: the library takes our enriched columns and data, returning a gridConfig object that contains:

  • State (who is expanded? which page are we looking at?).
  • “Prop Getters” (functions that generate the correct HTML attributes).

3. The Executors (Grid & PaginatedGrid) These components are “dumb” from a logic point of view but “expert” in layout. They receive everything they need from gridConfig and handle:

  • Iterating over rows and cells.
  • Calling prepareRow (necessary for react-table v7).
  • Managing conditional rendering of sub-rows (renderSubElement).

Why this separation?

  • Testability: You can test the mapping logic in GridContainer separately from the layout.
  • Maintainability: If you decide to change the table design (e.g., move from a bordered table to a “striped” one), you only need to modify Grid.tsx, without touching the data logic.
  • Performance: By using useMemo in GridElementContainer, we avoid heavy react-table recalculations on every parent render.

Conclusion

The implementation of tests with Grid.tsx and GridContainer.tsx with a Container/Presenter architecture demonstrates how a plugin-composition approach can solve complex data visualization scenarios. Moving to v8 would bring greater robustness thanks to TypeScript’s type system, while keeping the “headless” philosophy that makes this component so versatile.


  • Migration to @tanstack/react-table: To achieve better performance and a superior developer experience thanks to static types.
  • Virtualization: For tables with thousands of rows, integration with react-virtual (also by TanStack) would be the next logical step.
  • Server-side Logic: Move sorting and pagination server-side to improve initial load times on massive datasets.

About the author

For the last 20 years, Stefano Frasca has worked with a variety of web technologies both backend and frontend. He is currently focused on front-end development. On his day to day job, he is working as a FSBO Area Leader at Immobiliare Labs IT. He has worked remotely for years, passionate about photography, food and code 😎
Do you want to know more? Visit my website!