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é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

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 :

OptionDescription
width / minWidth / maxWidthLargeur de la colonne (valeur CSS)
defaultHiddenColonne masquée par défaut (avec enableColumnConfiguration)
disableColumnClickNeutralise onRowClick sur cette colonne
centerCentre 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éClientServerDescription
PaginationCôté client ou serveur
TriTri par colonne, valeurs par défaut configurables
Recherche globale✅ Fuzzy✅ BackendRecherche full-text
Prefilters✅ via prefilterFnOnglets de filtre rapides
Filtres dynamiquesDropdowns multi-sélection
Actions inlineClick, Link, Endpoint
Actions dropdownVia display: 'dropdown'
Actions dynamiquesAction calculée par ligne (icône, libellé, visibilité)
ConfirmationVia withConfirm, avec la dialog de votre choix
Config colonnesAfficher/masquer colonnes
Colonnes calculéesPréfixe _extra_ + searchableValue
Persistence URLPrefilter, pagination, recherche, filtres et tri dans l’URL
Row click / Active rowonRowClick et highlight via activeRowId
Custom renderJSX personnalisé par cellule
Topbar customRéorganisez la barre d’outils via renderTopbar
ResponsiveRendu adapté en mobile
Empty stateMessage personnalisable via texts
Loading stateSkeleton intégré
ThèmeVariables CSS + thème scopé par classe
i18nVia une fonction de traduction injectable

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

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

AvantAprè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 setupwithConfirm sur chaque action de type endpoint
confirm: { title, content } sur l’actionwithConfirm: 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 ?

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, 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.