Crear un scroll infinito con React y un custom hook
Se trata de cargar nuevos elementos en respuesta a un evento, y la manera más cómoda de hacerlo es mediante un{' '}
hook
Cargar datos a medida que el usuario lo pide, bien apretando un botón o bien cuando ocurra un evento de scroll es una manera excelente de evitar sobrecargar nuestro JS Bundle
y el número de nodos DOM
a procesar, algo que siempre tiene un impacto alto en los score de Lighthouse (y todo lo que ello implica para el posicionamiento web)
Por lo tanto, si nuestro caso aplica (no hay nada peor que optimizar cuando no es necesario), implementar un infinite scroll
es el camino a seguir
Opciones varias
Tenemos muchas opciones para implementar esto
La posiblemente mejor (por aquello de no reinventar la rueda y utilizar soluciones muy probadas y sólidas): utilizar librerías externas
Por ejemplo
- react-window
- react-list
- react-waypoint
- react-infinite-scroller
- react-infinite-scroll-component
- react-tiny-virtual-list
- react-simple-infinite-scroll
- react-infinity
Pero si no nos apetece, siempre podemos hacerlo nosotros mismos
Utilizando un hook
Un hook no es más que una manera de estructurar una función que nos facilita su encapsulamiento y su reutilización (en un contexto de React
)
El hook es el envoltorio, pero de qué irá el contenido?
Un infinite scrolling se refiere simplemente al hecho de cargar elementos en la medida en que los necesitemos, ya sea porque el usuario se está acercando al último elemento visible, o porque se aprieta un botón de "más entradas"
Para usos extremos no sólo necesitaríamos ir añadiendo esos nuevos elementos sino que también sería crítico ir eliminando los elementos primeros (que ya están fuera de la visión del usuario), para evitar sobrecargar el DOM
y que la página se vaya volviendo más y más pesada
Pero si nuestro contenedor no espera miles de elementos esta optimización es innecesaria
En este caso, pediremos una nueva imagen de picsum.photos
, haremos bloques de 10 y al final un botón para pedir otro bloque de 10, y así succesivamente
Estos bloques podrían ser artículos, temas, herramientas, y podríamos ya tener un número total de elementos
Una implementación simple sería la siguiente, sin hook
import React, { useState, useEffect } from 'react'
export default function App() {
const [images, setImages] = useState([])
const [blocks, setBlocks] = useState(1)
const more = async () => {
const more_images = await fetch_images(blocks + 1)
setBlocks(prev => prev + 1)
setImages(prev => [...prev, ...more_images])
}
const fetch_images = async page => (await fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)).json()
useEffect(() => {
;(async () => {
const picsum = await fetch_images(0)
setImages(picsum)
})()
}, [])
return (
<div className="App">
<div style={{ display: 'flex', flexDirection: 'column' }}>
{images.map((el, i) => (
<img
key={`img${i}`}
src={el.download_url}
style={{
width: '150px',
height: 'auto',
marginBottom: '10px',
borderRadius: '3px',
}}
alt=""
/>
))}
</div>
<button style={{ marginBottom: '100px' }} onClick={more}>
more images
</button>
</div>
)
}
Aquí cada vez que apretamos el botón pedimos nuevas imágenes a picsum
y se las añadimos al array, guardado en el state
images
, y listos
Si en lugar de apretar un botón queremos que sea automático al pasar por un elemento determinado (por ejemplo el penúltimo del array), yo lo haría con la librería react-intersection-observer
Pero sin entrar en esto, la gracia ahora es convertir el código de antes en un hook
Qué problema nos solucionaría un hook?
Básicamente simplificar el código, encapsularlo y sacarlo de aquí, porque si queremos utilizar la misma lógica en otro componente tendremos que replicar todas las funciones y contaminar ese código, algo que nunca será buena idea
Con el hook lo que queremos es una función que nos devuelva lo que necesitemos, y listos, y en este caso lo suyo sería que nos devolviese el array de imágenes, y la función more()
import React, { useState, useEffect } from 'react'
export default function App() {
const [images, more] = useInfinitePicsum()
return (
<div className="App">
<div style={{ display: 'flex', flexDirection: 'column' }}>
{images.map((el, i) => (
<img
key={`img${i}`}
src={el.download_url}
style={{
width: '150px',
height: 'auto',
marginBottom: '10px',
borderRadius: '3px',
}}
alt=""
/>
))}
</div>
<button style={{ marginBottom: '100px' }} onClick={more}>
more images
</button>
</div>
)
}
const useInfinitePicsum = () => {
// estado para almacenar las imágenes
const [images, setImages] = useState([])
// estado para contar el número de bloques que pido, lo necesito para que picsum no me devuelva siempre las mismas imágenes
const [blocks, setBlocks] = useState(1)
// función para pedir más imágenes
const more = async () => {
const more_images = await fetch_images(blocks + 1)
setBlocks(prev => prev + 1)
setImages(prev => [...prev, ...more_images])
}
// función para pedir (esta vez literalmente) las imagenes a picsum
const fetch_images = async page => (await fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)).json()
// useEffect para ejecutar al principio la primera "pedida" de imágenes
useEffect(() => {
;(async () => {
const picsum = await fetch_images(0)
setImages(picsum)
})()
}, [])
return [images, more]
}
Con esto la lógica React
de la función principal es muy fácil de entender, y la lógica del hook es la que encapsula todo lo que viene a ser el crear el infinite scroll
Listos!
🙋♂️