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épendance Version Usage
react >= 18.0 Framework UI
react-dom >= 18.0 DOM rendering
@tanstack/react-table >= 8.0 Logique de table (headless)
@tanstack/react-query >= 5.0 Gestion 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é Client Server Description
Pagination Côté client ou serveur
Tri Multi-colonnes supporté
Recherche globale ✅ Fuzzy ✅ Backend Recherche full-text
Prefilters Onglets de filtre rapides
Filtres dynamiques Dropdowns multi-sélection
Actions inline Click, Link, Endpoint
Actions dropdown Menu « … » pour économiser l’espace
Confirmation dialog Via useConfirm injectable
Config colonnes Afficher/masquer colonnes
Persistence URL Filtres dans l’URL
Active row Highlight de ligne
Custom render JSX personnalisé par cellule
Empty state Message personnalisable
Loading state Skeleton intégré
i18n Via hook injectable

Pourquoi pas X ?

Alternative Problème
AG Grid Payant, lourd, style imposé
MUI DataGrid Dépendance MUI, style imposé
Mantine DataTable Dépendance Mantine
TanStack Table seul Trop de boilerplate
Shadcn DataTable Copier-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.