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/snowtablePuis 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
- GitHub :
https://github.com/snowpact/snowtable - Démo live :
https://snowpact.github.io/snowtable/ - npm :
@snowpact/snowtable
Article rédigé par l’équipe Snowpact. Retrouvez nos projets open-source sur GitHub.
