Créer des tableaux triables avec React

Publié: 2022-03-10
Résumé rapide ↬ Rendre vos tableaux triables dans React peut sembler une tâche ardue, mais cela ne doit pas être trop difficile. Dans cet article, nous allons implémenter tout ce dont vous avez besoin pour trier tous vos besoins de tri de table.

Le tri des tables a toujours été un problème assez difficile à résoudre. Il y a beaucoup d'interactions à suivre, de nombreuses mutations DOM à faire et même des algorithmes de tri complexes. Ce n'est qu'un de ces défis difficiles à relever. Droit?

Au lieu d'utiliser des bibliothèques externes, essayons de créer des choses nous-mêmes. Dans cet article, nous allons créer un moyen réutilisable de trier vos données tabulaires dans React. Nous passerons en revue chaque étape en détail et apprendrons un tas de techniques utiles en cours de route.

Nous n'aborderons pas la syntaxe de base de React ou JavaScript, mais vous n'avez pas besoin d'être un expert en React pour suivre.

Créer une table avec React

Commençons par créer un exemple de composant de table. Il acceptera un tableau de produits et produira un tableau très basique, répertoriant une ligne par produit.

 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> ); }
Plus après saut! Continuez à lire ci-dessous ↓

Ici, nous acceptons une gamme de produits et les mettons en boucle dans notre table. C'est statique et non triable pour le moment, mais ça va pour le moment.

Trier les données

Si vous croiriez tous les enquêteurs du tableau blanc, vous penseriez que le développement de logiciels était presque tous des algorithmes de tri. Heureusement, nous n'examinerons pas ici un tri rapide ou un tri à bulles.

Le tri des données en JavaScript est assez simple, grâce à la fonction de tableau intégrée sort() . Il triera les tableaux de nombres et de chaînes sans argument supplémentaire :

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

Si vous voulez quelque chose d'un peu plus intelligent, vous pouvez lui passer une fonction de tri. Cette fonction reçoit deux éléments de la liste en tant qu'arguments et placera l'un devant l'autre en fonction de ce que vous décidez.

Commençons par trier les données que nous obtenons par ordre alphabétique de nom.

 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> ); }

Alors que se passe-t-il ici ? Tout d'abord, nous créons une copie de l'accessoire du produit, que nous pouvons modifier et modifier à notre guise. Nous devons le faire car la fonction Array.prototype.sort modifie le tableau d'origine au lieu de renvoyer une nouvelle copie triée.

Ensuite, nous appelons sortedProducts.sort et lui transmettons une fonction de sorting . Nous vérifions si la propriété name du premier argument a est avant le deuxième argument b , et si c'est le cas, renvoyons une valeur négative. Cela indique que a doit venir avant b dans la liste. Si le nom du premier argument est après le nom du deuxième argument, nous renvoyons un nombre positif, indiquant que nous devons placer b avant a . Si les deux sont égaux (c'est-à-dire qu'ils ont le même nom), nous renvoyons 0 pour préserver l'ordre.

Rendre notre table triable

Nous pouvons donc maintenant nous assurer que le tableau est trié par nom - mais comment pouvons-nous modifier nous-mêmes l'ordre de tri ?

Pour changer le champ par lequel nous trions, nous devons nous souvenir du champ actuellement trié. Nous allons le faire avec le crochet useState .

Un crochet est un type spécial de fonction qui nous permet de « s'accrocher » à certaines des fonctionnalités de base de React, comme la gestion de l'état et le déclenchement d'effets secondaires. Ce crochet particulier nous permet de conserver un élément d'état interne dans notre composant et de le modifier si nous le souhaitons. Voici ce que nous ajouterons :

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

On commence par ne rien trier du tout. Ensuite, modifions les en-têtes du tableau pour inclure un moyen de changer le champ par lequel nous voulons trier.

 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> ); };

Maintenant, chaque fois que nous cliquons sur un en-tête de tableau, nous mettons à jour le champ sur lequel nous voulons trier. Net-o !

Cependant, nous ne faisons pas encore de tri réel, alors corrigeons cela. Vous souvenez-vous de l'algorithme de tri d'avant ? Le voici, légèrement modifié pour fonctionner avec n'importe lequel de nos noms de champs.

 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>

Nous nous assurons d'abord que nous avons choisi un champ pour trier, et si c'est le cas, nous trions les produits par ce champ.

Ascendant vs Descendant

La prochaine fonctionnalité que nous voulons voir est un moyen de basculer entre l'ordre croissant et décroissant. Nous allons basculer entre l'ordre croissant et décroissant en cliquant une fois de plus sur l'en-tête du tableau.

Pour implémenter cela, nous devrons introduire un deuxième élément d'état - l'ordre de tri. Nous allons refactoriser notre variable d'état sortedField actuelle pour conserver à la fois le nom du champ et sa direction. Au lieu de contenir une chaîne, cette variable d'état contiendra un objet avec une clé (le nom du champ) et une direction. Nous allons le renommer en sortConfig pour être un peu plus clair.

Voici la nouvelle fonction de tri :

 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; });

Maintenant, si la direction est "ascendante", nous ferons comme nous l'avons fait précédemment. Si ce n'est pas le cas, nous ferons le contraire, en nous donnant un ordre décroissant.

Ensuite, nous allons créer une nouvelle fonction - requestSort - qui acceptera le nom du champ et mettra à jour l'état en conséquence.

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

Nous devrons également changer nos gestionnaires de clics pour utiliser cette nouvelle fonction !

 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> );

Maintenant, nous commençons à avoir l'air plutôt complets, mais il reste encore une grande chose à faire. Nous devons nous assurer que nous ne trions nos données que lorsque nous en avons besoin. Actuellement, nous trions toutes nos données sur chaque rendu, ce qui entraînera toutes sortes de problèmes de performances sur toute la ligne. Au lieu de cela, utilisons le crochet useMemo intégré pour mémoriser toutes les parties lentes !

 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]);

Si vous ne l'avez jamais vu auparavant, useMemo est un moyen de mettre en cache - ou de mémoriser - des calculs coûteux. Donc, étant donné la même entrée, il n'est pas nécessaire de trier les produits deux fois si nous restituons notre composant pour une raison quelconque. Notez que nous voulons déclencher un nouveau tri chaque fois que nos produits changent, ou que le champ ou la direction par laquelle nous trions change.

Envelopper notre code dans cette fonction aura d'énormes implications sur les performances de notre tri de table !

Rendre tout réutilisable

L'une des meilleures choses à propos des crochets est la facilité avec laquelle il est possible de rendre la logique réutilisable. Vous allez probablement trier tous les types de tables dans votre application, et devoir réimplémenter à nouveau les mêmes éléments ressemble à un frein.

React a cette fonctionnalité appelée crochets personnalisés. Ils ont l'air fantaisistes, mais ce ne sont que des fonctions régulières qui utilisent d'autres crochets à l'intérieur d'eux. Refactorisons notre code pour qu'il soit contenu dans un crochet personnalisé, afin que nous puissions l'utiliser partout !

 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 }; }

Il s'agit à peu près d'un copier-coller de notre code précédent, avec un peu de changement de nom. useSortableData accepte les éléments et un état de tri initial facultatif. Il renvoie un objet avec les éléments triés et une fonction pour trier à nouveau les éléments.

Notre code de table ressemble maintenant à ceci :

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

Une dernière touche

Il manque un petit morceau - un moyen d'indiquer comment le tableau est trié. Afin d'indiquer cela dans notre conception, nous devons également renvoyer l'état interne - le sortConfig . Renvoyons-le également et utilisons-le pour générer des styles que nous pouvons appliquer à nos en-têtes de tableau !

 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> ); };

Et avec ça, on a fini !

Emballer

Il s'avère que créer votre propre algorithme de tri de table n'était pas une tâche impossible après tout. Nous avons trouvé un moyen de modéliser notre état, nous avons écrit une fonction de tri générique et nous avons écrit un moyen de mettre à jour nos préférences de tri. Nous nous sommes assurés que tout était performant et avons tout refactorisé dans un hook personnalisé. Enfin, nous avons fourni un moyen d'indiquer l'ordre de tri à l'utilisateur.

Vous pouvez voir une démo de la table dans ce CodeSandbox :