Creación de tablas ordenables con React

Publicado: 2022-03-10
Resumen rápido ↬ Hacer que tus tablas se puedan ordenar en React puede parecer una tarea abrumadora, pero no tiene por qué ser demasiado difícil. En este artículo, implementaremos todo lo que necesita para resolver todas sus necesidades de clasificación de tablas.

La clasificación de tablas siempre ha sido un problema bastante difícil de hacer bien. Hay muchas interacciones para realizar un seguimiento, extensas mutaciones DOM para hacer e incluso algoritmos de clasificación intrincados también. Es solo uno de esos desafíos que son difíciles de hacer bien. ¿Derecha?

En lugar de extraer bibliotecas externas, intentemos hacer cosas nosotros mismos. En este artículo, vamos a crear una forma reutilizable de ordenar sus datos tabulares en React. Repasaremos cada paso en detalle y aprenderemos un montón de técnicas útiles en el camino.

No analizaremos la sintaxis básica de React o JavaScript, pero no es necesario ser un experto en React para seguirlo.

Creando una tabla con React

Primero, creemos un componente de tabla de muestra. Aceptará una variedad de productos y generará una tabla muy básica, enumerando una fila por producto.

 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> ); }
¡Más después del salto! Continúe leyendo a continuación ↓

Aquí, aceptamos una variedad de productos y los colocamos en nuestra mesa. Es estático y no ordenable en este momento, pero está bien por ahora.

Ordenando los datos

Si le creyera a todos los entrevistadores de la pizarra, pensaría que el desarrollo de software consiste casi en su totalidad en algoritmos de clasificación. Afortunadamente, aquí no analizaremos una ordenación rápida o una ordenación por burbujas.

Ordenar datos en JavaScript es bastante sencillo, gracias a la función de matriz integrada sort() . Ordenará matrices de números y cadenas sin un argumento adicional:

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

Si quieres algo un poco más inteligente, puedes pasarle una función de clasificación. Esta función recibe dos elementos de la lista como argumentos y colocará uno delante del otro según lo que decidas.

Comencemos por ordenar los datos que obtenemos alfabéticamente por nombre.

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

Entonces, ¿qué está pasando aquí? Primero, creamos una copia de la propiedad de productos, que podemos alterar y cambiar como queramos. Necesitamos hacer esto porque la función Array.prototype.sort altera la matriz original en lugar de devolver una nueva copia ordenada.

A continuación, llamamos sortedProducts.sort y le pasamos una función de sorting . Comprobamos si la propiedad de name del primer argumento a está antes del segundo argumento b , y si es así, devuelve un valor negativo. Esto indica que a debe ir antes de b en la lista. Si el nombre del primer argumento está después del nombre del segundo argumento, devolvemos un número positivo, lo que indica que debemos colocar b antes a . Si los dos son iguales (es decir, ambos tienen el mismo nombre), devolvemos 0 para conservar el orden.

Hacer que nuestra tabla se pueda ordenar

Entonces, ahora podemos asegurarnos de que la tabla esté ordenada por nombre, pero ¿cómo podemos cambiar el orden de clasificación nosotros mismos?

Para cambiar el campo por el que ordenamos, debemos recordar el campo ordenado actualmente. Lo haremos con el gancho useState .

Un gancho es un tipo especial de función que nos permite "engancharnos" en algunas de las funciones principales de React, como administrar el estado y desencadenar efectos secundarios. Este gancho en particular nos permite mantener una parte del estado interno en nuestro componente y cambiarlo si queremos. Esto es lo que agregaremos:

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

Empezamos por no ordenar nada en absoluto. A continuación, modifiquemos los encabezados de la tabla para incluir una forma de cambiar el campo por el que queremos ordenar.

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

Ahora, cada vez que hacemos clic en el encabezado de una tabla, actualizamos el campo por el que queremos ordenar. Genial-o!

Sin embargo, todavía no estamos haciendo ninguna clasificación real, así que arreglemos eso. ¿Recuerdas el algoritmo de clasificación de antes? Aquí está, ligeramente modificado para que funcione con cualquiera de nuestros nombres de 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>

Primero nos aseguramos de haber elegido un campo por el que ordenar y, de ser así, ordenamos los productos por ese campo.

Ascendente vs Descendente

La siguiente característica que queremos ver es una forma de cambiar entre orden ascendente y descendente. Cambiaremos entre orden ascendente y descendente haciendo clic en el encabezado de la tabla una vez más.

Para implementar esto, necesitaremos introducir un segundo estado: el orden de clasificación. Refactorizaremos nuestra variable de estado sortedField actual para mantener tanto el nombre del campo como su dirección. En lugar de contener una cadena, esta variable de estado contendrá un objeto con una clave (el nombre del campo) y una dirección. Le cambiaremos el nombre a sortConfig para que sea un poco más claro.

Aquí está la nueva función de clasificación:

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

Ahora, si la dirección es 'ascendente', haremos lo mismo que hicimos anteriormente. Si no es así, haremos lo contrario, dándonos un orden descendente.

A continuación, crearemos una nueva función, requestSort , que aceptará el nombre del campo y actualizará el estado en consecuencia.

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

¡También tendremos que cambiar nuestros manejadores de clics para usar esta nueva función!

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

Ahora empezamos a tener una apariencia bastante completa, pero todavía queda una cosa importante por hacer. Necesitamos asegurarnos de que solo clasificamos nuestros datos cuando es necesario. Actualmente, estamos clasificando todos nuestros datos en cada renderizado, lo que conducirá a todo tipo de problemas de rendimiento en el futuro. En su lugar, usemos el gancho useMemo para memorizar todas las partes lentas.

 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 no lo ha visto antes, useMemo es una forma de almacenar en caché, o memorizar, cálculos costosos. Entonces, dada la misma entrada, no tiene que ordenar los productos dos veces si volvemos a renderizar nuestro componente por alguna razón. Tenga en cuenta que queremos activar una nueva ordenación cada vez que cambien nuestros productos, o el campo o la dirección por la que ordenamos cambie.

¡Envolver nuestro código en esta función tendrá enormes implicaciones de rendimiento para nuestra clasificación de tablas!

Hacer que todo sea reutilizable

Una de las mejores cosas de los ganchos es lo fácil que es hacer que la lógica sea reutilizable. Probablemente ordenará todos los tipos de tablas en su aplicación, y tener que volver a implementar las mismas cosas de nuevo suena como un lastre.

React tiene esta característica llamada ganchos personalizados. Suenan elegantes, pero todo lo que son son funciones regulares que usan otros ganchos dentro de ellos. ¡Refactoricemos nuestro código para que esté contenido en un enlace personalizado, para que podamos usarlo en todas partes!

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

Esto es más o menos copiar y pegar de nuestro código anterior, con un poco de cambio de nombre incluido. useSortableData acepta los elementos y un estado de clasificación inicial opcional. Devuelve un objeto con los elementos ordenados y una función para volver a ordenar los elementos.

Nuestro código de tabla ahora se ve así:

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

Un último toque

Falta una pequeña pieza: una forma de indicar cómo se ordena la tabla. Para indicar eso en nuestro diseño, también debemos devolver el estado interno: sortConfig . ¡Recuperemos eso también y usémoslo para generar estilos que podamos aplicar a los encabezados de nuestras tablas!

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

Y con eso, ¡hemos terminado!

Terminando

Resulta que, después de todo, crear su propio algoritmo de clasificación de tablas no era una hazaña imposible. Encontramos una manera de modelar nuestro estado, escribimos una función de clasificación genérica y escribimos una forma de actualizar cuáles son nuestras preferencias de clasificación. Nos aseguramos de que todo funcionara y lo refactorizamos todo en un gancho personalizado. Finalmente, proporcionamos una forma de indicar el orden de clasificación al usuario.

Puedes ver una demostración de la tabla en este CodeSandbox: