DEV Community

Nathan Collins
Nathan Collins

Posted on

Building Powerful Data Tables with TanStack Table in React

TanStack Table (formerly React Table) is a headless, lightweight, and powerful table library for React that provides complete control over table rendering and behavior. It's framework-agnostic at its core but provides React-specific hooks and utilities. This guide walks through creating feature-rich, customizable data tables using TanStack Table with React, covering setup, column definitions, and practical implementation patterns. This is part 14 of a series on using TanStack Table with React.

Prerequisites

Before you begin, ensure you have:

  • Node.js version 16.0 or higher
  • npm, yarn, or pnpm package manager
  • A React project (version 16.8 or higher) with hooks support
  • Basic understanding of React hooks (useState, useMemo, useCallback)
  • Familiarity with TypeScript (recommended but not required)

Installation

Install TanStack Table React package:

npm install @tanstack/react-table
Enter fullscreen mode Exit fullscreen mode

Or with yarn:

yarn add @tanstack/react-table
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add @tanstack/react-table
Enter fullscreen mode Exit fullscreen mode

Your package.json should include:

{
  "dependencies": {
    "@tanstack/react-table": "^8.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Project Setup

TanStack Table is headless, meaning it doesn't provide any default styling. You'll need to create your own table markup and styles. No additional setup is required beyond installation.

First Example / Basic Usage

Let's create a basic table component. Create src/DataTable.jsx:

// src/DataTable.jsx
import React, { useMemo, useState } from 'react';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  flexRender,
} from '@tanstack/react-table';

function DataTable() {
  const [data] = useState([
    { id: 1, name: 'John Doe', email: 'john@example.com', age: 28, city: 'New York' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 32, city: 'London' },
    { id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 45, city: 'Paris' },
    { id: 4, name: 'Alice Williams', email: 'alice@example.com', age: 29, city: 'Tokyo' }
  ]);

  const columns = useMemo(() => [
    {
      accessorKey: 'id',
      header: 'ID',
    },
    {
      accessorKey: 'name',
      header: 'Name',
    },
    {
      accessorKey: 'email',
      header: 'Email',
    },
    {
      accessorKey: 'age',
      header: 'Age',
    },
    {
      accessorKey: 'city',
      header: 'City',
    },
  ], []);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  return (
    <div style={{ padding: '20px' }}>
      <h2>Employee Directory</h2>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  style={{
                    padding: '12px',
                    textAlign: 'left',
                    borderBottom: '2px solid #ddd',
                    cursor: header.column.getCanSort() ? 'pointer' : 'default',
                    userSelect: 'none'
                  }}
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                  {{
                    asc: '',
                    desc: '',
                  }[header.column.getIsSorted() as string] ?? null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(row => (
            <tr key={row.id} style={{ borderBottom: '1px solid #eee' }}>
              {row.getVisibleCells().map(cell => (
                <td key={cell.id} style={{ padding: '12px' }}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default DataTable;
Enter fullscreen mode Exit fullscreen mode

Update your App.jsx:

// src/App.jsx
import React from 'react';
import DataTable from './DataTable';
import './App.css';

function App() {
  return (
    <div className="App">
      <h1>TanStack Table Example</h1>
      <DataTable />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This creates a basic sortable table. Click column headers to sort.

Understanding the Basics

TanStack Table uses a hook-based API where:

  • useReactTable: Main hook that creates a table instance
  • columns: Array of column definitions using accessorKey or accessorFn
  • data: Array of row data objects
  • flexRender: Utility to render cell and header content
  • Row Models: Functions like getCoreRowModel(), getSortedRowModel() that enable features

Key concepts:

  • Headless: No default styling - you control all rendering
  • Column Definitions: Define structure with accessorKey, header, cell
  • Row Models: Enable features (sorting, filtering, pagination)
  • Table Instance: Returned from useReactTable, provides methods to render table

Here's an example with custom cell rendering:

// src/CustomTable.jsx
import React, { useMemo, useState } from 'react';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  flexRender,
} from '@tanstack/react-table';

function CustomTable() {
  const [data] = useState([
    { id: 1, product: 'Laptop', price: 999.99, stock: 15, status: 'In Stock' },
    { id: 2, product: 'Mouse', price: 29.99, stock: 8, status: 'Low Stock' },
    { id: 3, product: 'Keyboard', price: 79.99, stock: 12, status: 'In Stock' }
  ]);

  const columns = useMemo(() => [
    {
      accessorKey: 'id',
      header: 'ID',
    },
    {
      accessorKey: 'product',
      header: 'Product',
    },
    {
      accessorKey: 'price',
      header: 'Price',
      cell: ({ getValue }) => `$${getValue().toFixed(2)}`,
    },
    {
      accessorKey: 'stock',
      header: 'Stock',
      cell: ({ row }) => {
        const stock = row.getValue('stock');
        return (
          <span style={{ 
            color: stock < 10 ? 'red' : 'green',
            fontWeight: 'bold'
          }}>
            {stock}
          </span>
        );
      },
    },
    {
      accessorKey: 'status',
      header: 'Status',
      cell: ({ getValue }) => {
        const status = getValue();
        return (
          <span style={{
            padding: '4px 8px',
            borderRadius: '4px',
            backgroundColor: status === 'In Stock' ? '#d4edda' : '#fff3cd',
            color: status === 'In Stock' ? '#155724' : '#856404'
          }}>
            {status}
          </span>
        );
      },
    },
  ], []);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  return (
    <div style={{ padding: '20px' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  style={{
                    padding: '12px',
                    textAlign: 'left',
                    borderBottom: '2px solid #ddd',
                    cursor: header.column.getCanSort() ? 'pointer' : 'default'
                  }}
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {flexRender(header.column.columnDef.header, header.getContext())}
                  {{
                    asc: '',
                    desc: '',
                  }[header.column.getIsSorted() as string] ?? null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(row => (
            <tr key={row.id} style={{ borderBottom: '1px solid #eee' }}>
              {row.getVisibleCells().map(cell => (
                <td key={cell.id} style={{ padding: '12px' }}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default CustomTable;
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a comprehensive employee management table with sorting, filtering, and pagination:

// src/EmployeeManagement.jsx
import React, { useMemo, useState } from 'react';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  flexRender,
} from '@tanstack/react-table';

function EmployeeManagement() {
  const [data] = useState([
    { 
      id: 1, 
      name: 'Sarah Johnson', 
      email: 'sarah@example.com',
      department: 'Engineering', 
      salary: 95000, 
      startDate: '2020-01-15',
      status: 'Active'
    },
    { 
      id: 2, 
      name: 'Michael Chen', 
      email: 'michael@example.com',
      department: 'Marketing', 
      salary: 75000, 
      startDate: '2019-06-20',
      status: 'Active'
    },
    { 
      id: 3, 
      name: 'Emily Davis', 
      email: 'emily@example.com',
      department: 'Sales', 
      salary: 65000, 
      startDate: '2021-03-10',
      status: 'Active'
    },
    { 
      id: 4, 
      name: 'David Wilson', 
      email: 'david@example.com',
      department: 'Engineering', 
      salary: 110000, 
      startDate: '2018-09-05',
      status: 'Active'
    },
    { 
      id: 5, 
      name: 'Lisa Anderson', 
      email: 'lisa@example.com',
      department: 'HR', 
      salary: 70000, 
      startDate: '2022-01-08',
      status: 'Active'
    }
  ]);

  const [sorting, setSorting] = useState([]);
  const [globalFilter, setGlobalFilter] = useState('');

  const columns = useMemo(() => [
    {
      accessorKey: 'id',
      header: 'ID',
      enableSorting: true,
    },
    {
      accessorKey: 'name',
      header: 'Employee Name',
      enableSorting: true,
      cell: ({ row }) => {
        const name = row.getValue('name');
        const email = row.original.email;
        return (
          <div>
            <strong>{name}</strong>
            <div style={{ fontSize: '12px', color: '#666' }}>{email}</div>
          </div>
        );
      },
    },
    {
      accessorKey: 'department',
      header: 'Department',
      enableSorting: true,
      cell: ({ getValue }) => {
        const dept = getValue();
        const colors = {
          Engineering: '#dc3545',
          Marketing: '#ffc107',
          Sales: '#28a745',
          HR: '#17a2b8'
        };
        return (
          <span style={{
            padding: '4px 8px',
            borderRadius: '4px',
            backgroundColor: colors[dept] || '#6c757d',
            color: 'white',
            fontSize: '12px'
          }}>
            {dept}
          </span>
        );
      },
    },
    {
      accessorKey: 'salary',
      header: 'Salary',
      enableSorting: true,
      cell: ({ getValue }) => {
        const salary = getValue();
        return `$${salary.toLocaleString()}`;
      },
    },
    {
      accessorKey: 'startDate',
      header: 'Start Date',
      enableSorting: true,
      cell: ({ getValue }) => {
        const date = new Date(getValue());
        return date.toLocaleDateString('en-US');
      },
    },
    {
      accessorKey: 'status',
      header: 'Status',
      enableSorting: true,
      cell: ({ getValue }) => {
        const status = getValue();
        return (
          <span style={{
            padding: '4px 8px',
            borderRadius: '4px',
            backgroundColor: status === 'Active' ? '#d4edda' : '#f8d7da',
            color: status === 'Active' ? '#155724' : '#721c24',
            fontWeight: 'bold'
          }}>
            {status}
          </span>
        );
      },
    },
  ], []);

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      globalFilter,
    },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  });

  return (
    <div style={{ padding: '20px' }}>
      <h2>Employee Management</h2>

      {/* Global Filter */}
      <div style={{ marginBottom: '16px' }}>
        <input
          type="text"
          placeholder="Search employees..."
          value={globalFilter}
          onChange={(e) => setGlobalFilter(e.target.value)}
          style={{
            padding: '8px',
            width: '300px',
            border: '1px solid #ddd',
            borderRadius: '4px'
          }}
        />
      </div>

      {/* Table */}
      <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '16px' }}>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id} style={{ backgroundColor: '#f5f5f5' }}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  style={{
                    padding: '12px',
                    textAlign: 'left',
                    borderBottom: '2px solid #ddd',
                    cursor: header.column.getCanSort() ? 'pointer' : 'default',
                    userSelect: 'none'
                  }}
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                  {{
                    asc: '',
                    desc: '',
                  }[header.column.getIsSorted() as string] ?? null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(row => (
            <tr 
              key={row.id} 
              style={{ 
                borderBottom: '1px solid #eee',
                backgroundColor: row.getIsSelected() ? '#e3f2fd' : 'white'
              }}
            >
              {row.getVisibleCells().map(cell => (
                <td key={cell.id} style={{ padding: '12px' }}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>

      {/* Pagination */}
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <button
          onClick={() => table.setPageIndex(0)}
          disabled={!table.getCanPreviousPage()}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          {'<<'}
        </button>
        <button
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          {'<'}
        </button>
        <span style={{ margin: '0 8px' }}>
          Page{' '}
          <strong>
            {table.getState().pagination.pageIndex + 1} of{' '}
            {table.getPageCount()}
          </strong>
        </span>
        <button
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          {'>'}
        </button>
        <button
          onClick={() => table.setPageIndex(table.getPageCount() - 1)}
          disabled={!table.getCanNextPage()}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          {'>>'}
        </button>
        <select
          value={table.getState().pagination.pageSize}
          onChange={(e) => {
            table.setPageSize(Number(e.target.value));
          }}
          style={{ marginLeft: '16px', padding: '4px' }}
        >
          {[10, 20, 30, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
}

export default EmployeeManagement;
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Sorting with visual indicators
  • Global filtering/search
  • Pagination with page size selection
  • Custom cell rendering with React components
  • Conditional styling
  • Currency and date formatting
  • Status badges

Common Issues / Troubleshooting

  1. Infinite re-renders: Use useMemo for columns and useState with initializer function for data to ensure stable references. Avoid creating new arrays/objects on every render.

  2. Sorting not working: Make sure you've included getSortedRowModel() in your table configuration and set enableSorting: true in column definitions.

  3. Filtering not working: Include getFilteredRowModel() and manage filter state. Use onGlobalFilterChange for global search or column-specific filters.

  4. Pagination not working: Add getPaginationRowModel() to your table configuration. Implement pagination controls using table methods like nextPage(), previousPage(), etc.

  5. TypeScript errors: TanStack Table has excellent TypeScript support. Define your data types and use ColumnDef<YourDataType>[] for columns.

Next Steps

Now that you understand TanStack Table:

  • Explore advanced features like row selection, column resizing, and grouping
  • Implement column filtering and custom filter components
  • Add row expansion for nested data
  • Learn about virtual scrolling for large datasets
  • Explore server-side data loading and pagination
  • Add export functionality
  • Check the official documentation: https://tanstack.com/table/latest
  • Look for part 15 of this series for more advanced topics

Summary

You've learned how to set up TanStack Table and create feature-rich data tables with sorting, filtering, pagination, and custom rendering. The headless architecture provides complete control over table appearance and behavior, making it perfect for building highly customized data management interfaces.

SEO Keywords

TanStack Table React
TanStack Table tutorial
React table TanStack
TanStack Table installation
React data table headless
TanStack Table example
React table component
TanStack Table setup
React interactive table
TanStack Table sorting
React data grid
TanStack Table filtering
React table library
TanStack Table pagination
React table component tutorial

Top comments (0)