Cómo crear una tabla ordenable y filtrable en React – CodesCode
Aprende a crear un componente de tabla en React, capaz de ser ordenado y filtrado, que ayuda a agilizar el proceso de manejar grandes conjuntos de datos.
Las tablas dinámicas se utilizan con frecuencia en aplicaciones web para representar datos de forma estructurada. La clasificación y filtrado del conjunto de datos puede acelerar los procesos al trabajar con grandes conjuntos de datos. En este tutorial, veremos cómo crear un componente de tabla ordenable y filtrable en React.
Puedes encontrar el código fuente completo en un solo lugar alojado en GitHub. El resultado final se muestra a continuación.
- Requisitos Previos
- Configuración del Proyecto
- Creación del Componente
- Estilizando la Tabla
- Agregando Controles
- Filtrando la Tabla
- Ordenando la Tabla
- Manejando Filtros en Exceso
- Conclusión
Requisitos Previos
Antes de comenzar, este tutorial asume que tienes un conocimiento básico de HTML, CSS, JavaScript y React. Aunque repasaremos el proyecto paso a paso, no explicaremos conceptos básicos de React ni métodos de matriz de JavaScript en detalle. También usaremos TypeScript, pero se puede lograr lo mismo sin él. Dicho esto, empecemos a codificar.
Configuración del Proyecto
Para este proyecto, utilizaremos Vite, una herramienta frontal sólida y popular. Si aún no tienes una aplicación React existente, puedes iniciar un nuevo proyecto en Vite utilizando uno de los siguientes comandos en tu terminal:
# Using NPMnpm create vite@latest folder-name -- --template react-ts# Using Yarnyarn create vite folder-name --template react-ts# Using PNPMpnpm create vite folder-name --template react-ts# Using Bunbunx create-vite folder-name --template react-ts
Una vez que estés listo, configura una nueva carpeta para el componente Table
dentro del proyecto de React con la siguiente estructura:
src├─ components│ ├─ Table│ │ ├─ index.ts │ │ ├─ table.css│ │ ├─ Table.tsx├─ App.tsx
index.ts
. Utilizaremos este archivo para re-exportarTable.tsx
y simplificar las rutas de importación.table.css
. Contiene los estilos asociados con el componente. Para este tutorial, usaremos CSS “vanilla”.Table.tsx
. El componente en sí.
Abre Table.tsx
y exporta lo siguiente, para poder verificar que el componente se cargue cuando lo importemos:
import './table.css'export const Table = () => { return ( <h1>Table component</h1> )}
Dentro de index.ts
, vuelve a exportar el componente utilizando la siguiente línea:
export * from './Table'
Ahora que tenemos configurados los archivos del componente, verifiquemos que se cargue importándolo en nuestra aplicación. En este tutorial, utilizaremos el componente App
. Si ya tienes un proyecto de React existente, puedes importarlo en la ubicación deseada. Importa el componente Table
en tu aplicación de la siguiente manera:
import { Table } from './components/Table'const App = () => { return ( <Table /> )}export default App
Generando los datos simulados
Por supuesto, para trabajar en la tabla, primero necesitaremos algunos datos simulados. Para este tutorial, podemos usar Generador de JSON, un servicio gratuito para generar datos JSON aleatorios. Utilizaremos el siguiente esquema para generar los datos:
[ '{{repeat(10)}}', { id: '{{index()}}', name: '{{firstName()}} {{surname()}}', company: '{{company().toUpperCase()}}', active: '{{bool()}}', country: '{{country()}}' }]
JSON Generator viene con varias funcionalidades incorporadas para generar diferentes tipos de datos. El esquema anterior creará un array de objetos con diez objetos aleatorios en la siguiente forma:
{ id: 0, // number - Index of the array, starting from 0 name: 'Jaime Wallace', // string - A random name company: 'UBERLUX', // string - Capitalized random string active: false, // boolean - either `true` or `false` country: 'Peru' // string - A random country name}
Genera una lista de entradas utilizando el esquema de arriba, luego crea un nuevo archivo dentro de la carpeta src
llamado data.ts y exporta la matriz de la siguiente manera:
export const data = [ { id: 0, name: 'Jaime Wallace', company: 'UBERLUX', active: false, country: 'Peru' }, { ... },]
Abre App.tsx
, y pasa estos datos al componente Table
como una prop llamada rows
. Generaremos la tabla basándonos en estos datos:
import { Table } from './components/Table'+ import { data } from './data' const App = () => { return (- <Table />+ <Table rows={data} /> ) } export default App
Creando el componente
Ahora que tenemos tanto el componente como los datos configurados, podemos comenzar a trabajar en la tabla. Para generar dinámicamente la tabla basada en los datos pasados, reemplace todo en el componente Table
con las siguientes líneas de código:
import { useState } from 'react'import './table.css'export const Table = ({ rows }) => { const [sortedRows, setRows] = useState(rows) return ( <table> <thead> <tr> {Object.keys(rows[0]).map((entry, index) => ( <th key={index}>{entry}</th> ))} </tr> </thead> <tbody> {sortedRows.map((row, index) => ( <tr key={index}> {Object.values(row).map((entry, columnIndex) => ( <td key={columnIndex}>{entry}</td> ))} </tr> ))} </tbody> </table> )}
Esto generará dinámicamente tanto los encabezados de la tabla como las celdas basadas en la propiedad rows
. Vamos a desglosar cómo funciona. Como vamos a ordenar y filtrar las filas, necesitamos almacenarlas en un estado utilizando el gancho useState
. La propiedad se pasa como valor inicial al gancho.
Para mostrar los encabezados de la tabla, podemos usar Object.keys
en la primera entrada del array, lo cual devolverá las claves del objeto como una lista de cadenas:
const rows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1 Turn object properties into an array of keys:Object.keys(rows[0]) -> ['id', 'name']// #2 Chain `map` from the array to display the values inside `th` elements:['id', 'name'].map((entry, index) => (...))
Para mostrar las celdas de la tabla, necesitamos usar Object.values
en cada fila, lo cual retorna el valor de cada clave en un objeto, a diferencia de Object.keys
. En detalle, así es como mostramos las celdas de la tabla:
const sortedRows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1 Loop through each object in the array and create a `tr` element:{sortedRows.map((row, index) => (<tr key={index}>...</tr>))}// #2 Loop through each property of each object to create the `td` elements:Object.values(row) -> [0, 'Jaime Wallace']
Este enfoque lo hace extremadamente flexible para usar cualquier tipo de datos con nuestro componente Tabla
, sin tener que reescribir la lógica. Hasta ahora, tendremos la siguiente tabla creada utilizando nuestro componente. Sin embargo, hay algunos problemas con el formato.
Formateo de las celdas de la tabla
En este momento, la columna activa
no se muestra. Esto se debe a que los valores para esos campos son booleanos, y no se imprimen como cadenas en JSX. Para resolver este problema, podemos introducir una nueva función para formatear las entradas en función de sus valores. Agregue lo siguiente al componente Tabla
y envuelva entry
en la función en el JSX:
const formatEntry = (entry: string | number | boolean) => { if (typeof entry === 'boolean') { return entry ? '✅' : '❌' } return entry}return ( <table> <thead>...</thead> <tbody> {sortedRows.map((row, index) => ( <tr key={index}> {Object.values(row).map((entry, columnIndex) => ( <td key={columnIndex}>{formatEntry(entry)}</td> ))} </tr> ))} </tbody> </table>)
La función formatEntry
espera una entrada, que en nuestro caso puede ser string
, number
, o boolean
, y luego devuelve un valor formateado si el typeof entry
es un boolean
, lo que significa que para los valores true
, mostraremos una marca de verificación verde, y para los valores false
, mostraremos una cruz roja. Usando un enfoque similar, también podemos formatear los encabezados de la tabla. Hagámoslos en mayúsculas con la siguiente función:
export const capitalize = ( str: string) => str?.replace(/\b\w/g, substr => substr.toUpperCase())
Esta función utiliza una expresión regular para agarrar la primera letra de cada palabra y convertirla en mayúscula. Para utilizar esta función, podemos crear un archivo utils.ts
en la raíz de la carpeta src
, exportar esta función y luego importarla en nuestro componente Table
para usarla de la siguiente manera:
import { capitalize } from '../../utils'export const Table = ({ rows }) => { ... return ( <table> <thead> <tr> {Object.keys(rows[0]).map((entry, index) => ( <th key={index}>{capitalize(entry)}</th> ))} </tr> </thead> <tbody>...</tbody> </table> )}
Basado en estas modificaciones, ahora tenemos una tabla construida y formateada dinámicamente.
Propiedades de escritura
Antes de comenzar a estilizar la tabla y luego agregar controles, vamos a tipar adecuadamente la prop rows
. Para esto, podemos crear un archivo types.ts
en la raíz de la carpeta src
y exportar tipos personalizados que se puedan reutilizar en todo el proyecto. Crea el archivo y exporta el siguiente tipo:
export type Data = { id: number name: string company: string active: boolean country: string}[]
Para escribir la propiedad rows
en el componente Table
, simplemente importa este tipo y pásalo al componente de la siguiente manera:
import { Data } from '../../types'export type TableProps = { rows: Data}export const Table = ({ rows }: TableProps) => { ... }
Estilizando la tabla
Para estilizar el componente completo de la tabla, solo necesitaremos un par de reglas. Primero, queremos establecer los colores y los bordes, lo cual podemos hacer utilizando los siguientes estilos:
table { width: 100%; border-collapse: collapse;}thead { text-align: left; /* `thead` is centered by default */ color: #939393; background: #2f2f2f;}th,td { padding: 4px 6px; border: 1px solid #505050;}
Agrega lo anterior a table.css
. Asegúrate de establecer border-collapse
en collapse
en la etiqueta <table>
para evitar bordes dobles. Como la tabla abarca toda la pantalla, también hagamos algunos ajustes y eliminemos el borde izquierdo y derecho, ya que de todos modos no son visibles:
th:first-child,td:first-child { border-left: 0;}th:last-child,th:last-child { border-right: 0;}
Esto eliminará los bordes en cada lado de la <table>
, lo que resultará en un aspecto más limpio. Por último, vamos a agregar un efecto hover a las filas de la tabla para ayudar a los usuarios visualmente cuando están buscando en la tabla:
tr:hover { background: #2f2f2f;}
Con todo lo que se ha hecho hasta ahora, ahora tenemos el siguiente comportamiento para el componente.
Agregando Controles
Ahora que hemos estilizado la tabla, agreguemos los controles para la funcionalidad de ordenar y filtrar. Crearemos un <input>
para el filtro y un elemento <select>
para el ordenamiento. También incluiremos un botón para cambiar entre órdenes de ordenamiento (ascendente/descendente).
Para agregar los inputs, también necesitaremos nuevos estados para el orden actual (ascendente o descendente) y una variable para llevar el registro de la clave de ordenamiento (qué clave del objeto se utiliza para ordenar). Con eso en mente, amplía el componente Table
con lo siguiente:
const [order, setOrder] = useState('asc')const [sortKey, setSortKey] = useState(Object.keys(rows[0])[0])const filter = (event: React.ChangeEvent<HTMLInputElement>) => {}const sort = (value: keyof Data[0], order: string) => {}const updateOrder = () => {}return ( <> <div className="controls"> <input type="text" placeholder="Filter items" onChange={filter} /> <select onChange={(event) => sort()}> {Object.keys(rows[0]).map((entry, index) => ( <option value={entry} key={index}> Order by {capitalize(entry)} </option> ))} </select> <button onClick={updateOrder}>Switch order ({order})</button> </div> <table>...</table> </>)
Vamos en orden para entender qué cambió:
order
. Primero, necesitamos crear un nuevo estado para el ordenamiento. Este puede ser uno deasc
odesc
. Utilizaremos su valor en la funciónsort
.sortKey
. También necesitamos un estado para la clave de ordenamiento. Por defecto, podemos obtener la clave de la primera propiedad en nuestro array de objetos utilizandoObject.keys(rows[0])[0]
. Utilizaremos esto para mantener el seguimiento del orden cuando se cambie entre diferentes órdenes.filter
. Necesitaremos una función para filtrar los resultados. Esto debe ser pasado al eventoonChange
en el elemento<input>
. Ten en cuenta queReact.ChangeEvent
es un genérico y puede aceptar el tipo de elemento HTML que desencadenó el cambio.sort
. Al igual que la funciónfilter
, esto también deberá ser adjuntado al eventoonChange
, pero en este caso, en el elemento<select>
. Aceptará dos parámetros:
value
. Puede tomar las claves de nuestro objeto de datos. Podemos especificar el tipo usando la palabra clavekeyof
. Esto significa quevalue
puede ser uno deid
,name
,company
,active
ocountry
.order
. El orden del ordenamiento, ya seaasc
odesc
.
updateOrder
. Por último, también necesitamos una función para actualizar el orden. Esto se activará al hacer clic en el botón.Ten en cuenta que utilizamos la misma lógica que para los elementos <th>
para generar dinámicamente las opciones para el <select>
. También podemos reutilizar la función de utilidad capitalize
para formatear las opciones.
Estilizando los controles
Vamos a estilizar los controles antes de continuar. Esto se puede hacer con solo un puñado de reglas CSS. Amplía table.css
con lo siguiente:
.controls { display: flex;}input,select { flex: 1; padding: 5px 10px; border: 0;}button { background: #2f2f2f; color: #FFF; border: 0; cursor: pointer; padding: 5px 10px;}
Esto asegurará que los inputs estén alineados uno al lado del otro. Al utilizar flex: 1
en los elementos <input>
y <select>
, podemos hacer que ocupen el mismo ancho del espacio disponible. El <button>
ocupará tanto espacio como sea necesario para su texto.
Filtrando la Tabla
Ahora que tenemos los controles en su lugar, veamos cómo implementar la funcionalidad. Para filtrar la tabla basándonos en cualquier campo, deberemos seguir esta lógica:
const rows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1: Set `rows` to a filtered version using `filter`// The return value of `filter` will determine which rows to keepsetRows([ ...rows ].filter(row => { ... }))// From here on, we discuss the return value of `filter`// #2: Grab every field from the `row` object to use it for filteringObject.values(row) -> [0, 'Jaime Wallace']// #3: Join the values together into a single string[0, 'Jaime Wallace'].join('') -> '0Jaime Wallace'// #4: Convert the string into lowercase to make search case-insensitive'0Jaime Wallace'.toLowerCase() -> '0jaime wallace'// #5: Check if the string contains the value entered in the input'0jaime wallace'.includes(value) -> true / false
Con todo combinado, podemos crear el valor de retorno
para el filter
basado en la lógica anterior. Esto nos deja con la siguiente implementación para la función filter
:
const filter = (event: React.ChangeEvent<HTMLInputElement>) => { const value = event.target.value if (value) { setRows([ ...rows.filter(row => { return Object.values(row) .join('') .toLowerCase() .includes(value) }) ]) } else { setRows(rows) }}
Ten en cuenta que también queremos verificar si el value
está presente. Su ausencia significa que el campo <input>
está vacío. En tales casos, queremos restablecer el estado y pasar las filas sin filtrar rows
a setRows
para restablecer la tabla.
Ordenar la tabla
<pTenemos la funcionalidad de filtro, pero todavía nos falta ordenar. Para la ordenación, tenemos dos funciones separadas:
sort
. La función que se encargará de la ordenación.updateOrder
. La función que cambiará el orden de la ordenación de ascendente a descendente y viceversa.
Comencemos con la función de ordenación primero. Cada vez que cambie <select>
, se llamará a la función sort
. Queremos usar el valor del elemento <select>
para decidir qué clave usar para ordenar. Para esto, podemos utilizar un método de sort
simple y la notación de corchetes para comparar dinámicamente las claves del objeto:
const sort = (value: keyof Data[0], order: string) => { const returnValue = order === 'desc' ? 1 : -1 setSortKey(value) setRows([ ...sortedRows.sort((a, b) => { return a[value] > b[value] ? returnValue * -1 : returnValue }) ])}
Vamos a recorrer la función de arriba a abajo para entender mejor la implementación.
returnValue
. Basado en el estadoorder
, queremos que el valor de retorno sea 1 o -1. Esto nos ayuda a definir el orden de clasificación (1 para descendente y -1 para ascendente).setSortKey
. El valor pasado a la función es el valor del elemento<select>
. Queremos registrar este valor en nuestro estado (sortKey
), lo cual podemos hacer llamando a la función de actualizaciónsetSortKey
.setRows
. La clasificación real ocurre en esta llamada. Utilizando la notación de corchetes, podemos comparara[value]
conb[value]
y retornar -1 o 1.
Tomemos el siguiente ejemplo:
const rows = [{ id: 0 }, { id: 1 }]const value = 'id'// This translate to a.id and b.idrows.sort((a, b) => a[value] > b[value] ? -1 : 1)// If `returnValue` is -1, the order changesrows.sort((a, b) => a[value] > b[value] ? 1 : -1)
Cambiar entre órdenes de clasificación
Para actualizar el orden de clasificación, solo necesitamos actualizar el estado order
cada vez que se hace clic en el botón. Podemos lograr esto con la siguiente funcionalidad:
const updateOrder = () => { const updatedOrder = order === 'asc' ? 'desc' : 'asc' setOrder(updatedOrder) sort(sortKey as keyof Data[0], updatedOrder)}
Esto ajustará el orden
a su opuesto en cada clic. Ten en cuenta que después de actualizar el estado order
usando setOrder
, también necesitamos llamar a la función sort
para volver a ordenar la tabla según el orden actualizado. Para inferir el tipo correcto para la variable sortKey
, podemos hacer referencia a las claves del tipo de dato Data
usando la conversión de tipo: as keyof Data[0]
. Como segundo parámetro, también necesitamos pasar el orden actualizado.
Manejo de la sobre-filtración
Para completar este proyecto, agreguemos alguna indicación para un estado de sobre-filtración. Solo queremos mostrar un estado de sobre-filtración si no hay resultados. Esto se puede hacer fácilmente verificando la longitud
de nuestro estado sortedRows
. Después del elemento <table>
, agrega lo siguiente:
return ( <> <div className="controls">...</div> <table>...</table> {!sortedRows.length && ( <h1>No results... Try expanding the search</h1> )} </>)
Conclusión
En conclusión, construir una tabla clasificable y filtrable en React no tiene por qué ser complicado. Con los métodos de matriz y el encadenamiento de funciones, utilizando la funcionalidad adecuada, podemos crear funciones concisas y precisas para manejar estas tareas. Con todo incluido, logramos encajar toda la lógica en menos de 100 líneas de código.
Como se ha visto al principio de este tutorial, el proyecto completo está disponible en una sola pieza en GitHub. ¡Gracias por leer; feliz codificación!
Leave a Reply