Skip to main content

TL;DR : @snowpact/react-tanstack-query-table est un wrapper léger autour de TanStack
Table + TanStack Query qui élimine le boilerplate tout en vous laissant le contrôle total sur l’apparence.

Le problème des librairies « headless »

Si vous avez déjà utilisé TanStack Table (anciennement React Table), vous connaissez la situation :
une librairie puissante, flexible, avec zéro opinion sur le rendu. C’est génial en théorie.

En pratique ? Vous passez des heures à :

  • Écrire le même code de pagination sur chaque projet
  • Recréer les composants de tri, recherche, filtres
  • Gérer manuellement le state avec TanStack Query
  • Adapter les styles à votre design system (Shadcn, MUI, etc.)

Le résultat : un énorme boilerplate que vous copiez-collez de projet en projet. Et à chaque nouveau projet, vous
redécouvrez les mêmes edge cases.

Notre approche : Headless + Registry

Chez Snowpact, on a pris une approche différente. Au lieu de créer une librairie opinionated qui impose ses styles,
on a créé un système de registry :

  • Le package fournit la logique (pagination, tri, recherche, actions)
  • Vous injectez vos composants UI via une config unique
  • Vous personnalisez les styles Tailwind comme vous voulez

Résultat : 0 dépendance sur un design system, mais 0 boilerplate aussi.

Installation

npm install @snowpact/react-tanstack-query-table

Peer Dependencies

Le package s’intègre avec votre stack existante :

DépendanceVersionUsage
react>= 18.0Framework UI
react-dom>= 18.0DOM rendering
@tanstack/react-table>= 8.0Logique de table (headless)
@tanstack/react-query>= 5.0Gestion du state serveur

Quick Setup (5 minutes)

1. Configurez le registry une seule fois

// configs/setupSnowTable.tsx
import { setupSnowTable } from '@snowpact/react-tanstack-query-table';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';

import { useConfirm } from '@/components/ConfirmDialog';

export function setupSnowTableConfig() {
  setupSnowTable({
    // Traductions
    useTranslation: () => {
      const { t } = useTranslation();
      return { t };
    },

    // Links (react-router, Next.js, etc.)
    LinkComponent: Link,

    // Dialog de confirmation (optionnel)
    useConfirm: () => {
      const { confirm } = useConfirm();
      return { confirm };
    },

    // Personnalisation des styles Tailwind
    styles: {
      state: {
        active: 'ring-2 ring-primary/40',
        focus: 'ring-2 ring-primary/40',
      },
      table: {
        rowActive: 'bg-primary/10',
      },
    },
  });
}

2. Appelez le setup au démarrage

// main.tsx
import { setupSnowTableConfig } from './configs/setupSnowTable';

setupSnowTableConfig();

ReactDOM.createRoot(document.getElementById('root')!).render(
  <App />
);

3. Créez votre première table

// components/UserTable.tsx
import { SnowClientDataTable, SnowColumnConfig } from '@snowpact/react-tanstack-query-table';
import { Edit, Trash2 } from 'lucide-react';

type User = {
  id: string;
  name: string;
  email: string;
  createdAt: string;
};

const columns: SnowColumnConfig<User>[] = [
  { key: 'name', sortable: true },
  { key: 'email', sortable: true },
  {
    key: 'createdAt',
    label: 'Inscrit le',
    render: (user) => new Date(user.createdAt).toLocaleDateString(),
  },
];

export const UserTable = () => {
  return (
    <SnowClientDataTable
      queryKey={['users']}
      fetchAllItemsEndpoint={async () => {
        const res = await fetch('/api/users');
        return res.json();
      }}
      columnConfig={columns}
      actions={[
        {
          type: 'click',
          icon: Edit,
          label: 'Modifier',
          onClick: (user) => console.log('Edit', user),
        },
        {
          type: 'endpoint',
          icon: Trash2,
          label: 'Supprimer',
          variant: 'danger',
          endpoint: (user) => fetch(`/api/users/${user.id}`, { method: 'DELETE' }),
          confirm: {
            title: 'Confirmer la suppression',
            content: 'Cette action est irréversible.',
          },
        },
      ]}
      enableGlobalSearch
      enablePagination
      enableSorting
    />
  );
};

C’est tout. Pas de useState, pas de useEffect, pas de gestion manuelle du cache.

Client vs Server : Deux Modes

SnowClientDataTable

Charge toutes les données en une fois, puis filtre/trie côté client.

<SnowClientDataTable
  queryKey={['products']}
  fetchAllItemsEndpoint={fetchAllProducts}
  columnConfig={columns}
  enableGlobalSearch  // Recherche fuzzy côté client
  enablePagination    // Pagination côté client
/>

Idéal pour : Petits datasets (< 1000 items), admin panels, données qui changent peu.

SnowServerDataTable

Délègue pagination, tri et recherche au backend.

<SnowServerDataTable
  queryKey={['orders']}
  fetchServerEndpoint={async (params) => {
    // params: { limit, offset, search, sortBy, sortOrder, filters }
    const res = await api.getOrders(params);
    return {
      items: res.data.orders,
      totalItemCount: res.data.total,
    };
  }}
  columnConfig={columns}
  enableGlobalSearch
  enablePagination
/>

Idéal pour : Gros datasets, données sensibles, pagination SQL.

Fonctionnalités Avancées

Prefilters (Onglets de filtre)

<SnowServerDataTable
  prefilters={[
    { id: 'active', label: 'Actifs' },
    { id: 'archived', label: 'Archivés' },
    { id: 'all', label: 'Tous' },
  ]}
  fetchServerEndpoint={async (params) => {
    // params.prefilter = 'active' | 'archived' | 'all'
    return api.getUsers({ status: params.prefilter });
  }}
/>

Filtres Dynamiques

<SnowServerDataTable
  filters={[
    {
      key: 'status',
      label: 'Statut',
      options: [
        { label: 'En attente', value: 'pending' },
        { label: 'Validé', value: 'validated' },
      ],
      multipleSelection: true,
    },
    {
      key: 'category',
      label: 'Catégorie',
      options: categories.map(c => ({ label: c.name, value: c.id })),
    },
  ]}
/>

Configuration des Colonnes

<SnowClientDataTable
  enableColumnConfiguration  // Affiche le bouton :gear:
  columnConfig={[
    { key: 'id', meta: { defaultHidden: true } },  // Cachée par défaut
    { key: 'email', meta: { width: '200px' } },
  ]}
/>

Persistence du State

<SnowServerDataTable
  persistState  // Sauvegarde filtres/tri dans l'URL
  // L'utilisateur peut partager l'URL avec ses filtres
/>

Tailwind CSS v4 : Configuration

Si vous utilisez Tailwind v4, ajoutez la directive @source pour scanner les classes du package :

/* main.css */
@import 'tailwindcss';

/* Scanne les classes du package npm */
@source "../node_modules/@snowpact/react-tanstack-query-table/dist/index.js";

Tableau des Fonctionnalités

FonctionnalitéClientServerDescription
PaginationCôté client ou serveur
TriMulti-colonnes supporté
Recherche globale✅ Fuzzy✅ BackendRecherche full-text
PrefiltersOnglets de filtre rapides
Filtres dynamiquesDropdowns multi-sélection
Actions inlineClick, Link, Endpoint
Actions dropdownMenu « … » pour économiser l’espace
Confirmation dialogVia useConfirm injectable
Config colonnesAfficher/masquer colonnes
Persistence URLFiltres dans l’URL
Active rowHighlight de ligne
Custom renderJSX personnalisé par cellule
Empty stateMessage personnalisable
Loading stateSkeleton intégré
i18nVia hook injectable

Pourquoi pas X ?

AlternativeProblème
AG GridPayant, lourd, style imposé
MUI DataGridDépendance MUI, style imposé
Mantine DataTableDépendance Mantine
TanStack Table seulTrop de boilerplate
Shadcn DataTableCopier-coller, pas de package

SnowTable : Léger (< 50KB), aucune dépendance UI imposée, un seul setup.

Liens

Article rédigé par l’équipe Snowpact. Retrouvez nos projets open-source sur GitHub.