Skip to main content

TL;DR : @snowpact/snowtable 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.

Note : le package s’appelait auparavant @snowpact/react-tanstack-query-table. Il est désormais publié sous le nom @snowpact/snowtable. Voir la section « Migration » en fin d’article.

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 : Registry + variables CSS

Chez Snowpact, on a pris une approche différente. Au lieu de créer une librairie opinionated qui impose ses styles,
on combine deux mécanismes :

  • Le package fournit la logique et le rendu (pagination, tri, recherche, filtres, actions)
  • Vous injectez vos dépendances via un registry unique : traduction et composant Link
  • Vous adaptez l’apparence via des variables CSS (--snow-table-*) et, si besoin, un thème scopé par classe

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

Installation

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

Puis importez la feuille de style du package dans votre point d’entrée :

// main.tsx
import '@snowpact/snowtable/styles.css';

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

Les primitives d’interface (dropdown, select, tabs) sont fournies par Radix UI, embarqué en dépendance interne : aucune
librairie de design system à installer de votre côté.

Quick Setup (5 minutes)

1. Configurez le registry une seule fois

// configs/setupSnowTable.tsx
import { setupSnowTable } from '@snowpact/snowtable';
import { Link } from 'react-router-dom';

import { t } from './i18n';

export function setupSnowTableConfig() {
  setupSnowTable({
    // Fonction de traduction (requise)
    translate: (key) => t(key),

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

    // Surcharge des libellés d'interface, sans i18n complet (optionnel)
    translations: {
      'dataTable.search': 'Rechercher...',
      'dataTable.elements': 'éléments',
      'dataTable.resetFilters': 'Réinitialiser les filtres',
      'dataTable.searchEmpty': 'Aucun résultat',
    },

    // Classes CSS additionnelles sur certains composants (optionnel)
    styles: {
      searchBar: 'focus-visible:ring-2 focus-visible:ring-primary/40',
    },
  });
}

Les clés statiques (dataTable.*) ont des valeurs par défaut en anglais : si votre fonction
translate renvoie la clé telle quelle, le libellé par défaut est utilisé.

2. Appelez le setup au démarrage

// main.tsx
import '@snowpact/snowtable/styles.css';
import { resetSnowTable } from '@snowpact/snowtable';
import { setupSnowTableConfig } from './configs/setupSnowTable';

// Optionnel : permet au HMR de reprendre les changements de config
if (import.meta.hot) resetSnowTable();

setupSnowTableConfig();

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

3. Créez votre première table

// components/UserTable.tsx
import { SnowClientDataTable, SnowColumnConfig } from '@snowpact/snowtable';
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',
          className: 'destructive-button',
          endpoint: (user) => fetch(`/api/users/${user.id}`, { method: 'DELETE' }),
          withConfirm: async (user) => confirmDialog({
            title: 'Confirmer la suppression',
            content: `Supprimer ${user.name} ? Cette action est irréversible.`,
          }),
          onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
        },
      ]}
      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 : datasets modérés (< 5 000 items), admin panels, données qui changent peu.

SnowServerDataTable

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

import { SnowServerDataTable, ServerFetchParams } from '@snowpact/snowtable';

<SnowServerDataTable
  queryKey={['orders']}
  fetchServerEndpoint={async (params: ServerFetchParams) => {
    // params: { limit, offset, search?, prefilter?, filters?, sortBy?, sortOrder? }
    // sortOrder est en majuscules : 'ASC' | 'DESC'
    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

Actions : click, link, endpoint

Trois types d’actions, affichables en bouton ou dans un menu déroulant via display :

actions={[
  // Navigation via le LinkComponent du registry
  { type: 'link', icon: Eye, label: 'Voir', href: (user) => `/users/${user.id}`, external: false },

  // Appel API avec mutation gérée
  {
    type: 'endpoint',
    icon: Trash2,
    label: 'Supprimer',
    display: 'dropdown',
    endpoint: (user) => api.deleteUser(user.id),
    withConfirm: (user) => window.confirm(`Supprimer ${user.name} ?`),
    onSuccess: () => toast.success('Utilisateur supprimé'),
    onError: (error) => toast.error(error.message),
  },

  // Action dynamique, calculée par ligne
  (user) => ({
    type: 'click',
    icon: user.isActive ? Pause : Play,
    label: user.isActive ? 'Désactiver' : 'Activer',
    onClick: () => toggleStatus(user),
    hidden: user.role === 'admin',
  }),
]}

La confirmation se fait désormais par action, avec withConfirm : l’endpoint n’est appelé que si la
fonction renvoie true. Vous branchez la librairie de dialog de votre choix.

Prefilters (Onglets de filtre)

// Mode serveur : le prefilter actif est transmis au backend
<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 });
  }}
/>

// Mode client : vous fournissez la logique de filtrage
<SnowClientDataTable
  prefilters={[
    { id: 'all', label: 'Tous' },
    { id: 'active', label: 'Actifs' },
  ]}
  prefilterFn={(user, prefilterId) => prefilterId === 'all' || user.status === prefilterId}
/>

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 de configuration
  columnConfig={[
    { key: 'id', meta: { width: '80px', center: true, defaultHidden: true } },
    { key: 'email', meta: { minWidth: '200px', maxWidth: '320px' } },
    { key: 'actions', label: '', meta: { disableColumnClick: true } },
  ]}
/>

Les options de meta :

Option Description
width / minWidth / maxWidth Largeur de la colonne (valeur CSS)
defaultHidden Colonne masquée par défaut (avec enableColumnConfiguration)
disableColumnClick Neutralise onRowClick sur cette colonne
center Centre le contenu de la colonne

Colonnes calculées

Utilisez le préfixe _extra_ pour une colonne qui n’existe pas dans vos données :

const columns: SnowColumnConfig<User>[] = [
  {
    key: '_extra_fullName',
    label: 'Nom complet',
    render: (user) => `${user.firstName} ${user.lastName}`,
    searchableValue: (user) => `${user.firstName} ${user.lastName}`,
  },
];

Clic sur une ligne et ligne active

<SnowClientDataTable
  onRowClick={(user) => navigate(`/users/${user.id}`)}
  activeRowId={selectedUserId}
/>

Topbar personnalisée

<SnowClientDataTable
  renderTopbar={({ search, filters, columnConfiguration, resetFilters }) => (
    <div className="snow-topbar-right">
      {filters}
      <MyExportButton />
      {search}
      {columnConfiguration}
      {resetFilters}
    </div>
  )}
/>

Persistence du State

<SnowServerDataTable
  persistState  // Sauvegarde prefilter, pagination, recherche, filtres et tri dans l'URL
  // L'utilisateur peut partager l'URL avec ses filtres
/>

Personnaliser l’apparence

SnowTable embarque sa propre feuille de style. Pour l’accorder à votre design system, surchargez les variables CSS.
Elles sont déclarées avec @property : les valeurs que vous définissez avant l’import ne sont pas écrasées.

:root {
  --snow-table-background: #ffffff;   /* Fond principal */
  --snow-table-foreground: #0a0a0a;   /* Couleur de texte */
  --snow-table-primary: #525252;      /* Accent (focus, états actifs) */
  --snow-table-muted: #737373;        /* Texte secondaire */
  --snow-table-surface: #f5f5f5;      /* En-têtes, hover, skeleton */
  --snow-table-border: #e5e5e5;       /* Bordures */
  --snow-table-radius: 0.375rem;

  /* Optionnel */
  --snow-table-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --snow-table-row-even: transparent;
  --snow-table-action-surface: #f5f5f5;
}

/* Dark mode */
.dark {
  --snow-table-background: #1a1a2e;
  --snow-table-foreground: #eaeaea;
  --snow-table-primary: #3b82f6;
  --snow-table-surface: #16213e;
  --snow-table-border: #0f3460;
}

Thème scopé par classe

Pour aller plus loin (tailles, paddings, typographie), passez une className à la table et ciblez les
classes internes. La double spécificité l’emporte sur les styles par défaut : pas de !important, pas de
problème d’ordre de chargement, et plusieurs tables peuvent avoir des thèmes différents.

<SnowClientDataTable className="my-theme" ... />
.my-theme .snow-input { height: 36px; }
.my-theme .snow-table-header-cell { text-transform: uppercase; }
.my-theme .snow-table-cell { padding: 0.75rem 1rem; }

Tableau des Fonctionnalités

Fonctionnalité Client Server Description
Pagination ✅ ✅ Côté client ou serveur
Tri ✅ ✅ Tri par colonne, valeurs par défaut configurables
Recherche globale ✅ Fuzzy ✅ Backend Recherche full-text
Prefilters ✅ via prefilterFn ✅ Onglets de filtre rapides
Filtres dynamiques ✅ ✅ Dropdowns multi-sélection
Actions inline ✅ ✅ Click, Link, Endpoint
Actions dropdown ✅ ✅ Via display: 'dropdown'
Actions dynamiques ✅ ✅ Action calculée par ligne (icône, libellé, visibilité)
Confirmation ✅ ✅ Via withConfirm, avec la dialog de votre choix
Config colonnes ✅ ✅ Afficher/masquer colonnes
Colonnes calculées ✅ ✅ Préfixe _extra_ + searchableValue
Persistence URL ✅ ✅ Prefilter, pagination, recherche, filtres et tri dans l’URL
Row click / Active row ✅ ✅ onRowClick et highlight via activeRowId
Custom render ✅ ✅ JSX personnalisé par cellule
Topbar custom ✅ ✅ Réorganisez la barre d’outils via renderTopbar
Responsive ✅ ✅ Rendu adapté en mobile
Empty state ✅ ✅ Message personnalisable via texts
Loading state ✅ ✅ Skeleton intégré
Thème ✅ ✅ Variables CSS + thème scopé par classe
i18n ✅ ✅ Via une fonction de traduction injectable

Migration depuis @snowpact/react-tanstack-query-table

L’ancien package est déprécié. Quatre points à traiter :

Avant Après
@snowpact/react-tanstack-query-table @snowpact/snowtable (package.json + imports)
Directive Tailwind @source "…/dist/index.js" import '@snowpact/snowtable/styles.css' + variables --snow-table-*
useTranslation: () => ({ t }) translate: (key) => string — une fonction, plus un hook
useConfirm dans le setup withConfirm sur chaque action de type endpoint
confirm: { title, content } sur l’action withConfirm: async (item) => boolean
styles: { state, table } styles: { searchBar } + variables CSS pour le reste

Le reste de l’API (SnowClientDataTable, SnowServerDataTable, columnConfig,
filters, prefilters, persistState) est inchangé.

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, aucune dépendance de design system imposée, un seul setup.

Liens

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