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.

Componente de tabla final

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-exportar Table.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.

Problema de formato con el componente Tabla

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.

Tabla formateada en React

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.

Efecto de desplazamiento de tabla

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

Tabla con opciones de filtro

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 de asc o desc. Utilizaremos su valor en la función sort.
  • 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 utilizando Object.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 evento onChange en el elemento <input>. Ten en cuenta que React.ChangeEvent es un genérico y puede aceptar el tipo de elemento HTML que desencadenó el cambio.
  • sort. Al igual que la función filter, esto también deberá ser adjuntado al evento onChange, 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 clave keyof. Esto significa que value puede ser uno de id, name, company, active o country.
  • order. El orden del ordenamiento, ya sea asc o desc.
  • 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.

    Opciones select disponibles

    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.

    Filtrar 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 estado order, 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ón setSortKey.
    • setRows. La clasificación real ocurre en esta llamada. Utilizando la notación de corchetes, podemos comparar a[value] con b[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.

    Ordenando la tabla

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

    Estado sobrefiltrado

    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!

    Comparte este artículo


    Leave a Reply

    Your email address will not be published. Required fields are marked *