Nello sviluppo di dashboard enterprise, la gestione delle tabelle dati è spesso una delle sfide più complesse. In un mio progetto di prova, i componenti GridContainer.tsx e Grid.tsx rappresentano un eccellente esempio di come sfruttare la potenza di React Table (v7) per creare un’interfaccia flessibile, potente e altamente personalizzabile.
L’Approccio “Headless”
La particolarità di react-table è il suo essere una libreria headless. Non fornisce componenti UI (niente tag <table> pre-confezionati), ma mette a disposizione degli hook che gestiscono tutta la logica di stato: ordinamento, paginazione, espansione e selezione.
Nel test ho cercato di usare un’architettura con pattern Container/Presenter:
- GridContainer: Il “Cervello”. Decide quali colonne mostrare, come formattare i link e se attivare la paginazione.
- Grid: Il “Muscolo”. Prende le istruzioni e genera i tag HTML, gestisce i CSS e si assicura che l’utente veda fisicamente le icone di ordinamento o le righe espanse.
Componente GridContainer
In GridContainer.tsx, utilizziamo l’hook principale useTable combinandolo con diversi plugin:
const gridConfig = useTable(
gridData,
useSortBy, // Gestisce l'ordinamento delle colonne
useExpanded, // Gestisce l'espansione delle righe
!!withPagination && usePagination // Gestisce la paginazione (opzionale)
);Questa modularità permette di caricare solo il codice necessario, mantenendo il componente leggero.
Il “Trucco” del Pre-processing delle Colonne
Una delle feature più interessanti del nostro GridContainer è il modo in cui trasforma una definizione di dati “piatta” in una UI ricca. Prima di passare le colonne a react-table, il componente esegue un mapping dinamico:
tableData.columns = tableData.columns.map((el) => {
// 1. Iniezione dinamica dell'espansione
if (!!subRow && !!el.NeedCellExpand) {
el.Cell = ({ row }) => (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? '👇' : '👉'}
</span>
);
}
// 2. Rendering condizionale di link e badge
if (!!el.NeedCellLink) {
el.Cell = ({ row }) => (
<ButtonLink link={...} textButton={row.original[el.NeedCellLink]} />
);
}
return el;
});Particolarità: Row Expansion & Custom Cells
- Row Expansion: Grazie a
useExpanded, possiamo rendere le righe interattive. Il componenteGrid.tsxriceve una funzionerenderSubElementche permette di mostrare dettagli aggiuntivi sotto la riga principale senza sporcare la struttura della tabella madre. - Celle Polimorfiche: Invece di limitarci a mostrare testo, il componente trasforma i dati in
ButtonLink,Badgeo icone in base a flag specifici comeNeedMultipleLinkoNeedCellLink.
Evoluzione: Verso TanStack Table v8
Il mondo React corre veloce e react-table si è evoluto in TanStack Table v8. Se dovessimo aggiornare il nostro componente oggi, ecco le principali differenze che affronteremmo:
- TypeScript First: Mentre la v7 usa i plugin come argomenti di un hook, la v8 è stata completamente riscritta in TS. La definizione delle colonne diventerebbe più strutturata tramite un
ColumnHelper. - State Management: Nella v8, lo stato è più esplicito. Non si passano più i plugin come hook, ma si abilitano le “features” direttamente nell’oggetto di configurazione di
useReactTable. - Modularità Estrema: La v8 è ancora più leggera e rimuove completamente il concetto di “plugin” a favore di una configurazione dichiarativa.
Esempio di Refactoring (v7 vs v8)
Oggi (v7):
useTable({ columns, data }, useSortBy, usePagination)Domani (v8):
useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), // Feature di ordinamento esplicita
getPaginationRowModel: getPaginationRowModel(), // Feature di paginazione
})Componente Grid
Il componente Grid.tsx agisce come il Presentation Layer (o strato di visualizzazione) del sistema. Mentre GridContainer si occupa della logica di business e della configurazione dei plugin, Grid ha il compito di trasformare i dati elaborati da react-table in vero codice HTML.
Ecco di cosa si occupa nello specifico:
1. Rendering della Struttura HTML <table>
È qui che avviene il “matrimonio” tra la logica headless e il DOM. Il componente applica i cosiddetti prop getters forniti da react-table per garantire che la tabella sia accessibile e correttamente strutturata:
<table {...getTableProps()} className={css['grid--bordered']}>
<thead>{/* ... */}</thead>
<tbody {...getTableBodyProps()}>{/* ... */}</tbody>
</table>2. Gestione dell’Interfaccia di Ordinamento
Grid.tsx si occupa di mostrare visivamente lo stato dell’ordinamento nelle testate delle colonne, aggiungendo indicatori visivi (frecce) in base allo stato isSorted e isSortedDesc:
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>3. Ciclo di Vita delle Righe (prepareRow)
Un compito critico è l’esecuzione di prepareRow(row). In react-table, le righe vengono caricate pigramente (lazy); Grid prepara ogni riga appena prima del rendering, calcolando gli stili e le proprietà necessarie per le celle.
4. Rendering delle Sotto-Righe (Expanded Rows)
Il componente implementa la logica di visualizzazione per i dettagli aggiuntivi. Se una riga è espandibile e lo stato isExpanded è attivo, Grid.tsx inietta una riga extra nel <tbody> per renderizzare il renderSubElement:
{row.isExpanded ? (
<tr>
<td colSpan={visibleColumns.length}>
{renderSubElement({ row, ...otherInfo })}
</td>
</tr>
) : null}5. Debugging e Trasparenza
Include una modalità di debug che permette di visualizzare lo stato JSON interno dell’espansione, molto utile in fase di sviluppo per capire come sta reagendo il motore di react-table alle interazioni dell’utente.
Analisi visuale
Ecco una descrizione del flusso dei dati dalla sorgente fino al rendering finale nel DOM, dove si evidenzia il ruolo centrale della dipendenza react-table di TanStack.
1. Il Trasformatore (GridContainer)
Il GridContainer.tsx riceve i dati grezzi. Prima ancora che la libreria entri in gioco, esegue un “arricchimento” delle colonne. Se una colonna ha il flag NeedCellLink, viene iniettata una funzione Cell che renderizza un componente React (ButtonLink). Questo è un pattern potente: i dati decidono il loro aspetto.
2. Il Motore (react-table)
In GridElementContainer, richiamiamo gli hook di react-table. Qui avviene la magia: la libreria prende le nostre colonne arricchite e i dati, restituendoci un oggetto gridConfig che contiene:
- Lo stato (chi è espanso? quale pagina stiamo guardando?).
- I “Prop Getters” (funzioni che generano gli attributi HTML corretti).
3. Gli Esecutori (Grid & PaginatedGrid)
Questi componenti sono “stupidi” dal punto di vista della logica, ma “esperti” di layout. Ricevono tutto il necessario da gridConfig e si occupano di:
- Iterare sulle righe e le celle.
- Chiamare
prepareRow(necessario perreact-tablev7). - Gestire il rendering condizionale delle sotto-righe (
renderSubElement).
Perché questa separazione?
- Testabilità: Puoi testare la logica di mapping in
GridContainerseparatamente dal layout. - Manutenibilità: Se decidi di cambiare il design delle tabelle (es. passare da una tabella bordata a una “striped”), devi modificare solo
Grid.tsx, senza toccare la logica dei dati. - Performance: Usando
useMemoinGridElementContainer, evitiamo ricalcoli pesanti direact-tablead ogni render del genitore.
Conclusione
L’implementazione di test con Grid.tsx e GridContainer.tsx con un’architettura Container/Presenter dimostra come un approccio basato sulla composizione di plugin possa risolvere scenari complessi di visualizzazione dati. Il passaggio alla v8 porterebbe una maggiore robustezza grazie al sistema di tipi di TypeScript, pur mantenendo intatta la filosofia “headless” che rende questo componente così versatile.
Ottimizzazioni consigliate
- Migrazione a
@tanstack/react-table: Per ottenere migliori performance e una developer experience superiore grazie ai tipi statici. - Virtualizzazione: Per tabelle con migliaia di righe, l’integrazione con
react-virtual(sempre di TanStack) sarebbe il passo logico successivo. - Server-side Logic: Spostare l’ordinamento e la paginazione lato server per migliorare i tempi di caricamento iniziale su dataset massivi.