12k
All articles

Construire un Data Grid avec TanStack Table

Créez une grille de données TanStack Table avec tri, filtre, pagination et virtualisation, plus les correctifs pour les bugs de mémoïsation et rerenders infinis.

OpenReplay Team
OpenReplay Team
Construire un Data Grid avec TanStack Table

TanStack Table est une bibliothèque headless : elle prend en charge la logique du tableau (tri, filtrage, pagination et modèles de lignes) tandis que vous gardez le contrôle total du balisage et du style.

Si vous avez déjà passé un après-midi à vous battre contre la feuille de style d’une grille préconstruite juste pour ajuster une bordure, cet échange prendra immédiatement tout son sens. Vous obtenez un moteur d’état éprouvé et une page blanche pour le DOM, ce qui est exactement ce qu’il vous faut lorsque le design system vous appartient.

Ce guide met en place un véritable data grid sur la version stable actuelle, @tanstack/react-table v8 : un tableau unique capable de trier, de filtrer globalement et de paginer. Puis viennent les deux problèmes de montée en charge qui se manifestent réellement en production : le bug de mémoïsation qui provoque des re-rendus infinis, et la virtualisation pour des milliers de lignes.

Points clés à retenir

  • Dans TanStack Table v8, affichez chaque en-tête et chaque cellule avec flexRender : flexRender(header.column.columnDef.header, header.getContext()) pour les en-têtes et flexRender(cell.column.columnDef.cell, cell.getContext()) pour les cellules. Les appels cell.render('Cell') et column.render('Header') de l’ère v7 n’existent plus.
  • Le tri, le filtrage et la pagination sont des modèles de lignes optionnels : passez getSortedRowModel(), getFilteredRowModel() et getPaginationRowModel() dans un seul appel useReactTable et l’ordre n’a plus d’importance.
  • Le bug le plus courant avec TanStack Table est une boucle de re-rendu infinie causée par le passage d’un nouveau tableau data ou columns à chaque rendu. Encapsulez les deux dans useMemo ou définissez-les à la portée du module.
  • Au-delà de quelques milliers de lignes côté client, arrêtez de paginer et virtualisez avec useVirtualizer de @tanstack/react-virtual appliqué à table.getRowModel().rows.
  • La version stable est la v8, actuellement 8.21.3 sur le tag latest de npm. La v9 est entrée en bêta en juin 2026, donc maintenez vos tableaux de production sur la v8.

Ce que signifie « headless » et pourquoi c’est important

Headless signifie que la bibliothèque fournit le comportement, pas l’interface utilisateur. TanStack Table calcule les modèles de lignes et expose l’état ainsi que les gestionnaires d’événements, mais ne place rien dans le DOM : la v8 a cessé de fournir des styles par défaut et des attributs role afin que le noyau reste agnostique vis-à-vis du framework. L’avantage est un contrôle total. La contrepartie est que vous êtes également responsable de la sémantique : les éléments <table>, <th scope> et tout attribut ARIA sont à votre charge.

Concrètement, cela signifie que le style est entièrement à votre discrétion : classes utilitaires Tailwind, CSS Modules ou styled components sur votre propre balisage <table>. La bibliothèque n’y touche jamais.

Mettre en place un data grid TanStack Table de base

Installez le package et construisez le tableau avec le hook useReactTable. Définissez les colonnes avec createColumnHelper pour bénéficier de l’inférence de types, passez vos data et columns, et enregistrez getCoreRowModel(), le modèle de lignes de base qui mappe vos données en lignes.

npm install @tanstack/react-table

L’adaptateur prend en charge toutes les versions de React de la 16.8 à la 19, avec la réserve qu’il peut ne pas fonctionner correctement avec le compilateur React livré avec React 19.

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

type User = { firstName: string; lastName: string; age: number }

const columnHelper = createColumnHelper<User>()

const columns = [
  columnHelper.accessor('firstName', { header: 'First Name' }),
  columnHelper.accessor('lastName', { header: 'Last Name' }),
  columnHelper.accessor('age', { header: 'Age' }),
]

Effectuez le rendu en parcourant getHeaderGroups(), getRowModel().rows et les getVisibleCells() de chaque ligne. Passez la définition de colonne et son contexte à flexRender afin que les en-têtes sous forme de chaîne, les éléments JSX et les cellules composants soient tous résolus correctement. Conformément au guide de migration v8, flexRender remplace les appels supprimés cell.render('Cell') / column.render('Header').

function DataGrid({ data }: { data: User[] }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(hg => (
          <tr key={hg.id}>
            {hg.headers.map(header => (
              <th key={header.id}>
                {header.isPlaceholder
                  ? null
                  : flexRender(header.column.columnDef.header, header.getContext())}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(row => (
          <tr key={row.id}>
            {row.getVisibleCells().map(cell => (
              <td key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

Ajouter le tri

Activez le tri en ajoutant getSortedRowModel(), en maintenant l’état sorting avec onSortingChange, et en connectant chaque en-tête à header.column.getToggleSortingHandler(). Utilisez header.column.getIsSorted() (qui retourne 'asc', 'desc' ou false) pour afficher l’indicateur de direction.

const [sorting, setSorting] = useState<SortingState>([])

const table = useReactTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
})
<th onClick={header.column.getToggleSortingHandler()} style={{ cursor: 'pointer' }}>
  {flexRender(header.column.columnDef.header, header.getContext())}
  {{ asc: '', desc: '' }[header.column.getIsSorted() as string] ?? ''}
</th>

Filtrage et recherche globale

La recherche globale repose sur un seul élément d’état : ajoutez getFilteredRowModel(), conservez une chaîne globalFilter dans state, et mettez-la à jour avec onGlobalFilterChange. Le filtre global par défaut effectue une correspondance par sous-chaîne sur toutes les colonnes.

const [globalFilter, setGlobalFilter] = useState('')

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

// <input value={globalFilter} onChange={e => setGlobalFilter(e.target.value)} />

Les filtres par colonne utilisent le même modèle de lignes, pilotés par column.getFilterValue() et column.setFilterValue() depuis un champ de saisie dans l’en-tête. La correspondance approximative (fuzzy matching) est disponible mais nécessite le module auxiliaire séparé @tanstack/match-sorter-utils en tant que filterFn personnalisé. N’y recourez que lorsque la correspondance par sous-chaîne est insuffisante.

Pagination

Ajoutez getPaginationRowModel() et pilotez-le avec l’état pageIndex/pageSize, puis conditionnez vos boutons Suivant/Précédent avec table.getCanNextPage() et table.getCanPreviousPage(). L’ordre d’enregistrement des modèles de lignes n’a pas d’importance en v8, ils se composent donc librement avec le tri et le filtrage dans le même appel au hook.

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

const table = useReactTable({
  data,
  columns,
  state: { sorting, globalFilter, pagination },
  onSortingChange: setSorting,
  onGlobalFilterChange: setGlobalFilter,
  onPaginationChange: setPagination,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
})
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
  Previous
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
  Next
</button>

Vous disposez ainsi d’un data grid complet et fonctionnel : un seul appel à useReactTable qui trie, filtre et pagine simultanément.

Montée en charge : le piège de la mémoïsation, la virtualisation et les cellules personnalisées

Le bug le plus courant avec TanStack Table est une boucle de re-rendu infinie causée par le passage d’un nouveau tableau data ou columns à chaque rendu. Déclarer const columns = [...] ou const data = [...] dans le corps du composant crée une nouvelle référence à chaque passage ; TanStack Table détecte le changement d’identité, recalcule les modèles de lignes, déclenche un re-rendu, et le cycle se répète. Encapsulez les deux dans useMemo (ou définissez-les à la portée du module, comme dans les exemples précédents) afin que leurs références restent stables.

const columns = useMemo(() => [/* ... */], [])
const data = useMemo(() => fetchedRows, [fetchedRows])

Paginer ou virtualiser ? Paginéz lorsque les tailles de page sont bornées ou que les données sont récupérées page par page depuis un serveur. Virtualisez lorsque vous gérez des milliers de lignes côté client dans une vue à défilement continu. Dans ce cas, affichez uniquement les lignes visibles avec @tanstack/react-virtual et son hook useVirtualizer appliqué à table.getRowModel().rows. Notez que le hook actuel de la v3 est useVirtualizer, et non l’ancien useVirtual.

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

const rows = table.getRowModel().rows
const parentRef = useRef<HTMLDivElement>(null)

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 40,
  overscan: 10,
})

Vous affichez ensuite rowVirtualizer.getVirtualItems() à l’intérieur d’un espaceur dimensionné par getTotalSize(), comme le montrent la documentation de TanStack Virtual. C’est précisément le type de défaillance que la rejouabilité de session est efficace pour mettre en évidence : une grille de 10 000 lignes non virtualisée semble fonctionner correctement sur un jeu de données de test en local, mais rejouer un utilisateur réel effectuant un tri ou un défilement rapide en production révèle les pertes de frames et les saccades au défilement.

Les cellules personnalisées se définissent simplement via le renderer cell de la colonne : retournez un badge de statut, une date formatée ou un champ de saisie éditable, et la bibliothèque ne s’interpose pas, car elle ne rend jamais d’interface utilisateur elle-même. L’épinglage de colonnes, le redimensionnement et la réorganisation sont également pris en charge via leurs propres API d’état.

Conclusion

Vous disposez désormais d’un data grid qui trie, filtre et pagine dans un seul appel à useReactTable, ainsi que des deux techniques qui le maintiennent performant : des références stables pour data et columns, et la virtualisation au-delà de quelques milliers de lignes. Développez sur la v8 aujourd’hui, puisque la v9 est en bêta depuis juin 2026, et adoptez useVirtualizer dès que votre nombre de lignes dépasse la capacité d’une page.

FAQ

Pourquoi mon TanStack Table effectue-t-il des re-rendus infinis ?

Une boucle de re-rendu infinie provient presque toujours du passage d'un nouveau tableau data ou columns à chaque rendu. Déclarer const columns = [] ou const data = [] dans le corps du composant crée une nouvelle référence à chaque passage, ce qui amène TanStack Table à détecter le changement d'identité, à recalculer ses modèles de lignes et à déclencher un nouveau rendu. Encapsulez les deux dans useMemo ou définissez-les à la portée du module afin que leurs références restent stables.

Dois-je utiliser TanStack Table v8 ou v9 ?

Utilisez la v8 en production. La version stable est la v8 (8.21.3 sur le tag latest de npm), tandis que la v9 est en bêta depuis juin 2026 et ne se distribue encore qu'en builds de préversion sous le tag beta de npm. La v8 fonctionne avec React 16.8, 17, 18 et 19. Maintenez vos tableaux de production sur la v8 et n'expérimentez la v9 que dans du code non critique jusqu'à ce qu'elle atteigne une version stable.

Quand faut-il paginer plutôt que virtualiser un TanStack Table ?

Paginéz lorsque les tailles de page sont bornées ou que les données sont récupérées page par page depuis un serveur, en utilisant getPaginationRowModel et l'état pageIndex/pageSize. Virtualisez lorsque vous gérez des milliers de lignes côté client dans une vue à défilement continu : affichez uniquement les lignes visibles avec useVirtualizer de @tanstack/react-virtual appliqué à table.getRowModel().rows. Les deux approches ne sont pas mutuellement exclusives, mais au-delà de quelques milliers de lignes côté client dans une zone de défilement unique, la virtualisation est la bonne solution.

TanStack Table fournit-il des styles par défaut ou des rôles d'accessibilité ?

Non. TanStack Table v8 est entièrement headless. Elle ne produit ni balisage, ni style, ni attributs role ; la réécriture v8 a supprimé tout cela afin que le noyau puisse fonctionner avec n'importe quel framework. Vous êtes propriétaire de l'intégralité du DOM, donc le balisage sémantique comme table, th avec scope, et tout attribut ARIA relèvent de votre responsabilité. Stylisez-le avec Tailwind, CSS Modules ou styled components sur votre propre balisage.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.