Ordenar listas con JavaScript, sort(), React y un custom hook
Ordenar listas es fácil con sort()
, y con con React también es fácil con useState()
, y si además
queremos reutilizar la rutina para siempre, más que fácil con un custom hook
Función sort()
Para ordenar listas, nada más sencillo que utilizar la función sort()
, que funciona (puedes mirarlo aquí) simplemente comparando valores:
const lista = [2, 5, 4, 7]
console.log(lista.sort())
// [2, 4, 5, 7]
Esto de arriba es lo mismo que hacer lo siguiente
const lista = [2, 5, 4, 7]
console.log(lista.sort((a, b) => a - b))
// [2, 4, 5, 7]
sort()
invoca una función que recibe dos valores, y estos son los que compararemos
Si no son números y queremos un orden alfabético, haríamos lo siguiente
const lista = ['juan', 'palomo', 'y', 'sus cosas']
console.log(
lista.sort((a, b) => {
if (a > b) return 1
if (a < b) return -1
return 0
})
)
// ["juan", "palomo", "sus cosas", "y"]
Pero no hace falta escribir tanto, podemos expresar lo mismo con una línea
const lista = ['juan', 'palomo', 'y', 'sus cosas']
console.log(lista.sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)))
// ["juan", "palomo", "sus cosas", "y"]
Y si lo que tenemos son estructuras complejas, pues lo mismo
const lista = [
{ nombre: 'juan', edad: 50 },
{ nombre: 'alberto', edad: 40 },
{ nombre: 'aragor', edad: 2000 },
]
console.log(lista.sort((a, b) => (a.edad > b.edad ? 1 : a.edad < b.edad ? -1 : 0)))
// 0: {nombre: "alberto", edad: 40}
// 1: {nombre: "juan", edad: 50}
// 2: {nombre: "aragor", edad: 2000}
Bien
El tema es, y si esta lista la queremos mostrar en el front end?
Ningún problema, con React es tan sencillo como utilizar un estado, y en cuanto actualicemos el estado por lo que sea (un botón que se clicke, o simplemente al principio de todo) pues la lista se re-renderizará sola
Haré las dos cosas, que se ordene al principio de todo y con un botón, y con el botón cada vez la ordenaré al revés de lo que estaba
La función sort
me muta el array, es decir que me modifica el array en lugar de devolver uno nuevo
Para evitarlo hago una copia del array antes de aplicar sort
La razón es porque cambiar los objetos acostumbra a ser un generador de bugs
import React, { useState, useEffect } from 'react'
// mi componente principal <Lista>
const Lista = () => {
// guardo el estado list de valor inicial la lista que tengo
const [list, setList] = useState([
{ nombre: 'juan', edad: 50 },
{ nombre: 'alberto', edad: 40 },
{ nombre: 'aragor', edad: 2000 },
])
// utilizo useEffect para ejecutar este código sólo una vez
useEffect(() => {
// copio la lista con [...list] y la ordeno con sort()
const sortedList = [...list].sort((a, b) => (a.edad > b.edad ? 1 : a.edad < b.edad ? -1 : 0))
// actualizo el estado con la nueva lista ya ordenada
setList(sortedList)
}, [])
// vuelco el contenido del estado `list`
return (
<>
{/* Aquí pongo el botón para reordenar la lista */}
<button
onClick={() => {
let newSortedList = [...list].sort((a, b) => (a.edad > b.edad ? 1 : a.edad < b.edad ? -1 : 0))
// si la lista después de ordenarla tiene el mismo primer elemento, lo repito a la inversa
// (claro que esto es ineficiente, lo suyo sería habilitar otro estado para guardar el tipo de ordenamiento que hemos hecho)
if (newSortedList[0] === list[0])
newSortedList = [...list].sort((b, a) => (a.edad > b.edad ? 1 : a.edad < b.edad ? -1 : 0))
setList(newSortedList)
}}
>
Ordenar
</button>
{/* Y aquí la lista, cada vez que el estado cambie este componente se va a repintar y a actualizar la vista */}
<ul>
{list.map(el => (
<li>
{el.nombre}: {el.edad}
</li>
))}
</ul>
</>
)
}
export default Lista
Listos! Ya estamos ordenando a la manera React
, nada de tocar el DOM sino trabajar con datos, actualizar los datos, y dejar que React actualize el DOM cuando estos datos cambien
Y si queremos reaprovechar esta función?
Para eso tenemos los custom hooks, que no son más que maneras de encapsular código para que su reutilización sea la mar de cómoda
Opción 1: Sin custom hooks, una función al uso
import React, { useState, useEffect } from 'react'
// Esta es mi función para reutilizar `sort`
const sort_lists = (key, list, inverse) =>
inverse
? [...list].sort((b, a) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0))
: [...list].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0))
// Y este mi componente principal
const Lista = () => {
const [list, setList] = useState([
{ nombre: 'juan', edad: 50 },
{ nombre: 'alberto', edad: 40 },
{ nombre: 'aragor', edad: 2000 },
])
useEffect(() => {
setList(sort_lists('edad', list))
}, [])
return (
<>
<button
onClick={() => {
let newSortedList = sort_lists('edad', list)
if (newSortedList[0] === list[0]) newSortedList = sort_lists('edad', list, true)
setList(newSortedList)
}}
>
Ordenar
</button>
<ul>
{list.map(el => (
<li>
{el.nombre}: {el.edad}
</li>
))}
</ul>
</>
)
}
export default Lista
Ahora si queremos reordenar listas, basta con que copiemos la función sort_lists()
en nuestro código y ya lo tendremos
Eso sí, en realidad hemos encapsulado exclusivamente la funcionalidad para ordenar listas
En el código seguimos necesitando implantar la lógica de utilizar un useEffect
para ordenar la lista al principio, y un useState
para guardar la lista en un estado
Y si pudiésemos también encapsular esta lógica?
Sí que podemos, vamos con el custom hook
Opción 2: Con un custom hook
En lugar de definir una función, definimos un hook
que como tal tiene que empezar con el useLoQueSea
import React, { useState, useEffect } from 'react'
// Este es mi custom hook
const useSortTable = (listToSort, originalKey) => {
// definimos un estado
const [list, setList] = useState(listToSort)
// definimos la función anterior pero sin especificar la lista ya que será la principal
const sort_lists = (key, inverse) =>
inverse
? [...list].sort((b, a) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0))
: [...list].sort((a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0))
// ordenamos la lista con el useEffect
useEffect(() => {
setList(sort_lists(originalKey))
}, [])
// devolvemos el estado que contiene la lista
// ..el método para actualizar el estado
// ..y el método para ordenarla
return [list, setList, sort_lists]
}
// Ahora seguimos con el componente principal
const Lista = () => {
// y aquí utilizamos el hook
const [list, setList, sort] = useSortTable(
[
{ nombre: 'juan', edad: 50 },
{ nombre: 'alberto', edad: 40 },
{ nombre: 'aragor', edad: 2000 },
],
'edad'
)
return (
<>
<button
onClick={() => {
let newSortedList = sort('edad')
if (newSortedList[0] === list[0]) newSortedList = sort('edad', true)
setList(newSortedList)
}}
>
Ordenar
</button>
<ul>
{list.map(el => (
<li>
{el.nombre}: {el.edad}
</li>
))}
</ul>
</>
)
}
export default Lista
Si te fijas verás que el hook
en realidad engloba la función de antes y también la lógica del useEffect
y useState
Qué le falta a este código?
- La lógica para invertir la búsqueda es ineficiente
Si queremos un botón que nos ordene para una dirección o para otra, la manera es almacenar la dirección en otro estado y en función de esto ordenarla de una manera u otra
Aquí la estamos ordenando dos veces
- La lógica para invertir la búsqueda no está bien integrada en el hook
Lo suyo sería tener una función para invertir la búsqueda y que esta función también te actualizase el estado, algo tipo
<button onClick={sort}>Ordenar</button>
Se puede hacer sin problemas siempre que antes se solucione el problema anterior
- La función
list.map
genera componentes que no tienen unakey
asociada, y por eso nos sale el warning de rigor
Hace falta añadir la key
manualmente en estos casos
{
list.map((el, i) => (
<li key={`lista${i}`}>
{el.nombre}: {el.edad}
</li>
))
}
Listos!
🙋♂️