Creazione di tabelle ordinabili con React

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Rendere ordinabili le tabelle in React potrebbe sembrare un compito arduo, ma non deve essere troppo difficile. In questo articolo, implementeremo tutto ciò di cui hai bisogno per risolvere tutte le tue esigenze di ordinamento delle tabelle.

L'ordinamento delle tabelle è sempre stato un problema piuttosto difficile da correggere. Ci sono molte interazioni di cui tenere traccia, ampie mutazioni DOM da fare e persino intricati algoritmi di ordinamento. È solo una di quelle sfide difficili da risolvere. Destra?

Invece di estrarre librerie esterne, proviamo a creare cose da soli. In questo articolo, creeremo un modo riutilizzabile per ordinare i tuoi dati tabulari in React. Analizzeremo ogni passaggio in dettaglio e impareremo una serie di tecniche utili lungo il percorso.

Non esamineremo la sintassi di base di React o JavaScript, ma non è necessario essere un esperto di React per seguire.

Creazione di una tabella con React

Innanzitutto, creiamo un componente di tabella di esempio. Accetterà una serie di prodotti e produrrà una tabella molto semplice, elencando una riga per prodotto.

 function ProductTable(props) { const { products } = props; return ( <table> <caption>Our products</caption> <thead> <tr> <th>Name</th> <th>Price</th> <th>In Stock</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td>{product.stock}</td> </tr> ))} </tbody> </table> ); }
Altro dopo il salto! Continua a leggere sotto ↓

Qui, accettiamo una serie di prodotti e li inseriamo nella nostra tabella. È statico e non ordinabile al momento, ma per ora va bene.

Ordinamento dei dati

Se credessi a tutti gli intervistatori della lavagna, penseresti che lo sviluppo del software fosse quasi tutti algoritmi di smistamento. Fortunatamente, qui non esamineremo un ordinamento rapido o un ordinamento a bolle.

L'ordinamento dei dati in JavaScript è piuttosto semplice, grazie alla funzione di matrice integrata sort() . Ordina matrici di numeri e stringhe senza un argomento aggiuntivo:

 const array = ['mozzarella', 'gouda', 'cheddar']; array.sort(); console.log(array); // ['cheddar', 'gouda', 'mozzarella']

Se vuoi qualcosa di un po' più intelligente, puoi passargli una funzione di ordinamento. A questa funzione vengono assegnati due elementi nell'elenco come argomenti e li posizionerà uno davanti all'altro in base a ciò che si decide.

Iniziamo ordinando i dati che otteniamo in ordine alfabetico per nome.

 function ProductTable(props) { const { products } = props; let sortedProducts = [...products]; sortedProducts.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); return ( <Table> {/* as before */} </Table> ); }

Allora cosa sta succedendo qui? Per prima cosa, creiamo una copia del prodotto prop, che possiamo alterare e cambiare a nostro piacimento. Dobbiamo farlo perché la funzione Array.prototype.sort altera l'array originale invece di restituire una nuova copia ordinata.

Successivamente, chiamiamo sortedProducts.sort e gli passiamo una funzione di sorting . Verifichiamo se la proprietà name del primo argomento a è prima del secondo argomento b e, in tal caso, restituiamo un valore negativo. Ciò indica che a deve precedere b nell'elenco. Se il nome del primo argomento è dopo il nome del secondo argomento, restituiamo un numero positivo, indicando che dobbiamo mettere b prima a . Se i due sono uguali (cioè entrambi hanno lo stesso nome), restituiamo 0 per mantenere l'ordine.

Rendere la nostra tavola ordinabile

Quindi ora possiamo assicurarci che la tabella sia ordinata per nome, ma come possiamo modificare noi stessi l'ordine di ordinamento?

Per modificare il campo in base al quale ordiniamo, dobbiamo ricordare il campo attualmente ordinato. Lo faremo con l'hook useState .

Un hook è un tipo speciale di funzione che ci consente di "agganciare" alcune delle funzionalità principali di React, come la gestione dello stato e l'attivazione degli effetti collaterali. Questo particolare hook ci consente di mantenere un pezzo di stato interno nel nostro componente e di cambiarlo se vogliamo. Questo è ciò che aggiungeremo:

 const [sortedField, setSortedField] = React.useState(null);

Iniziamo dal non ordinare nulla. Quindi, modifichiamo le intestazioni della tabella per includere un modo per cambiare il campo in base al quale vogliamo ordinare.

 const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); return ( <table> <thead> <tr> <th> <button type="button" onClick={() => setSortedField('name')}> Name </button> </th> <th> <button type="button" onClick={() => setSortedField('price')}> Price </button> </th> <th> <button type="button" onClick={() => setSortedField('stock')}> In Stock </button> </th> </tr> </thead> {/* As before */} </table> ); };

Ora, ogni volta che facciamo clic sull'intestazione di una tabella, aggiorniamo il campo in base al quale vogliamo ordinare. Pulito!

Tuttavia, non stiamo ancora eseguendo alcun ordinamento effettivo, quindi risolviamolo. Ricordi l'algoritmo di ordinamento di prima? Eccolo, solo leggermente modificato per funzionare con uno qualsiasi dei nostri nomi di campo.

 const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortedField] < b[sortedField]) { return -1; } if (a[sortedField] > b[sortedField]) { return 1; } return 0; }); } return ( <table>

Per prima cosa ci assicuriamo di aver scelto un campo in base al quale ordinare e, in tal caso, ordiniamo i prodotti in base a quel campo.

Ascendente vs Discendente

La prossima caratteristica che vogliamo vedere è un modo per passare dall'ordine crescente a quello decrescente. Passeremo dall'ordine crescente a quello decrescente facendo clic sull'intestazione della tabella ancora una volta.

Per implementarlo, dovremo introdurre un secondo pezzo di stato: l'ordinamento. Ridimensioneremo la nostra attuale variabile di stato sortedField per mantenere sia il nome del campo che la sua direzione. Invece di contenere una stringa, questa variabile di stato conterrà un oggetto con una chiave (il nome del campo) e una direzione. Lo rinomineremo in sortConfig per essere un po' più chiaro.

Ecco la nuova funzione di ordinamento:

 sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; });

Ora, se la direzione è "ascendente", faremo come abbiamo fatto in precedenza. In caso contrario, faremo il contrario, dandoci un ordinamento decrescente.

Successivamente, creeremo una nuova funzione, requestSort , che accetterà il nome del campo e aggiornerà lo stato di conseguenza.

 const requestSort = key => { let direction = 'ascending'; if (sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); }

Dovremo anche cambiare i nostri gestori di clic per utilizzare questa nuova funzione!

 return ( <table> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')}> Name </button> </th> <th> <button type="button" onClick={() => requestSort('price')}> Price </button> </th> <th> <button type="button" onClick={() => requestSort('stock')}> In Stock </button> </th> </tr> </thead> {/* as before */} </table> );

Ora stiamo iniziando a sembrare piuttosto completi di funzionalità, ma c'è ancora una grande cosa da fare. Dobbiamo assicurarci di ordinare i nostri dati solo quando necessario. Attualmente, stiamo ordinando tutti i nostri dati su ogni rendering, il che porterà a tutti i tipi di problemi di prestazioni su tutta la linea. Invece, usiamo il hook useMemo per memorizzare tutte le parti lente!

 const ProductsTable = (props) => { const { products } = props; const [sortConfig, setSortConfig] = React.useState(null); React.useMemo(() => { let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortedProducts; }, [products, sortConfig]);

Se non l'hai mai visto prima, useMemo è un modo per memorizzare nella cache - o memorizzare - calcoli costosi. Quindi, dato lo stesso input, non è necessario ordinare i prodotti due volte se per qualche motivo eseguiamo nuovamente il rendering del nostro componente. Tieni presente che desideriamo attivare un nuovo ordinamento ogni volta che i nostri prodotti cambiano o cambia il campo o la direzione in cui ordiniamo.

Il wrapping del nostro codice in questa funzione avrà enormi implicazioni sulle prestazioni per l'ordinamento delle tabelle!

Rendendolo tutto riutilizzabile

Una delle cose migliori degli hook è quanto sia facile rendere la logica riutilizzabile. Probabilmente ordinerai tutti i tipi di tabelle in tutta la tua applicazione e dover reimplementare di nuovo le stesse cose suona come una seccatura.

React ha questa funzione chiamata hook personalizzati. Sembrano fantasiosi, ma sono tutte funzioni regolari che utilizzano altri ganci al loro interno. Ridimensioniamo il nostro codice in modo che sia contenuto in un hook personalizzato, così possiamo usarlo ovunque!

 const useSortableData = (items, config = null) => { const [sortConfig, setSortConfig] = React.useState(config); const sortedItems = React.useMemo(() => { let sortableItems = [...items]; if (sortConfig !== null) { sortableItems.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortableItems; }, [items, sortConfig]); const requestSort = key => { let direction = 'ascending'; if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); } return { items: sortedItems, requestSort }; }

Questo è praticamente un copia e incolla dal nostro codice precedente, con un po' di ridenominazione. useSortableData accetta gli elementi e uno stato di ordinamento iniziale opzionale. Restituisce un oggetto con gli elementi ordinati e una funzione per riordinare gli elementi.

Il nostro codice tabella ora appare così:

 const ProductsTable = (props) => { const { products } = props; const { items, requestSort } = useSortableData(products); return ( <table>{/* ... */}</table> ); };

Un ultimo tocco

Manca un minuscolo pezzo: un modo per indicare come è ordinato il tavolo. Per indicare che nel nostro progetto, dobbiamo restituire anche lo stato interno, il sortConfig . Restituiamo anche quello e usiamolo per generare stili che possiamo applicare alle intestazioni delle nostre tabelle!

 const ProductTable = (props) => { const { items, requestSort, sortConfig } = useSortableData(props.products); const getClassNamesFor = (name) => { if (!sortConfig) { return; } return sortConfig.key === name ? sortConfig.direction : undefined; }; return ( <table> <caption>Products</caption> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')} className={getClassNamesFor('name')} > Name </button> </th> {/* … */} </tr> </thead> {/* … */} </table> ); };

E con questo, abbiamo finito!

Avvolgendo

A quanto pare, creare il proprio algoritmo di ordinamento delle tabelle non era un'impresa impossibile dopotutto. Abbiamo trovato un modo per modellare il nostro stato, abbiamo scritto una funzione di ordinamento generica e abbiamo scritto un modo per aggiornare le nostre preferenze di ordinamento. Ci siamo assicurati che tutto fosse performante e abbiamo rifattorizzato il tutto in un hook personalizzato. Infine, abbiamo fornito un modo per indicare l'ordinamento all'utente.

Puoi vedere una demo della tabella in questo CodeSandbox: