Implementación de desplazamiento infinito e imagen Lazy Loading en React
Publicado: 2022-03-10HTML
Intersection Observer
para implementar el desplazamiento infinito y la carga diferida de imágenes en un componente funcional de React. En el proceso, aprenderemos cómo usar algunos de los ganchos de React y cómo crear ganchos personalizados.Si ha estado buscando una alternativa a la paginación, el desplazamiento infinito es una buena opción. En este artículo, vamos a explorar algunos casos de uso de la API Intersection Observer en el contexto de un componente funcional de React. El lector debe poseer un conocimiento práctico de los componentes funcionales de React. Un poco de familiaridad con los ganchos de React será beneficioso pero no obligatorio, ya que veremos algunos.
Nuestro objetivo es que, al final de este artículo, habremos implementado el desplazamiento infinito y la carga diferida de imágenes mediante una API HTML nativa. También habríamos aprendido algunas cosas más sobre React Hooks. Con eso, puede implementar el desplazamiento infinito y la carga diferida de imágenes en su aplicación React cuando sea necesario.
Empecemos.
Creación de mapas con React y Leaflet
Obtener información de un archivo CSV o JSON no solo es complicado, sino también tedioso. Representar los mismos datos en forma de ayuda visual es más sencillo. Shajia Abidi explica cuán poderosa es la herramienta Leaflet y cómo se pueden crear muchos tipos diferentes de mapas. Leer un artículo relacionado →
La API del observador de intersecciones
De acuerdo con los documentos de MDN, "la API Intersection Observer proporciona una forma de observar de forma asincrónica los cambios en la intersección de un elemento de destino con un elemento ancestro o con la ventana gráfica de un documento de nivel superior".
Esta API nos permite implementar características geniales como el desplazamiento infinito y la carga diferida de imágenes. El observador de intersección se crea llamando a su constructor y pasándole una devolución de llamada y un objeto de opciones. La devolución de llamada se invoca cada vez que un elemento, denominado target
, se cruza con la ventana gráfica del dispositivo o con un elemento específico, denominado root
. Podemos especificar una raíz personalizada en el argumento de opciones o usar el valor predeterminado.
let observer = new IntersectionObserver(callback, options);
La API es fácil de usar. Un ejemplo típico se ve así:
var intObserver = new IntersectionObserver(entries => { entries.forEach(entry => { console.log(entry) console.log(entry.isIntersecting) // returns true if the target intersects the root element }) }, { // default options } ); let target = document.querySelector('#targetId'); intObserver.observe(target); // start observation
entries
son una lista de objetos IntersectionObserverEntry
. El objeto IntersectionObserverEntry
describe un cambio de intersección para un elemento objetivo observado. Tenga en cuenta que la devolución de llamada no debe manejar ninguna tarea que consuma mucho tiempo, ya que se ejecuta en el subproceso principal.
La API de Intersection Observer actualmente disfruta de un amplio soporte de navegador, como se muestra en caniuse.
Puede leer más sobre la API en los enlaces proporcionados en la sección de recursos.
Veamos ahora cómo hacer uso de esta API en una aplicación React real. La versión final de nuestra aplicación será una página de imágenes que se desplaza infinitamente y tendrá cada imagen cargada lentamente.
Realización de llamadas a la API con el gancho useEffect
Para comenzar, clone el proyecto inicial desde esta URL. Tiene una configuración mínima y algunos estilos definidos. También agregué un enlace al CSS de Bootstrap
en el archivo public/index.html
, ya que usaré sus clases para diseñar.
Siéntase libre de crear un nuevo proyecto si lo desea. Asegúrese de tener instalado el administrador de paquetes de yarn
si desea seguir con el repositorio. Puede encontrar las instrucciones de instalación para su sistema operativo específico aquí.
Para este tutorial, tomaremos imágenes de una API pública y las mostraremos en la página. Usaremos las API de Lorem Picsum.
Para este tutorial, usaremos el punto final, https://picsum.photos/v2/list?page=0&limit=10
, que devuelve una matriz de objetos de imagen. Para obtener las siguientes diez imágenes, cambiamos el valor de página a 1, luego a 2, y así sucesivamente.
Ahora construiremos el componente de la aplicación pieza por pieza.
Abra src/App.js
e ingrese el siguiente código.
import React, { useEffect, useReducer } from 'react'; import './index.css'; function App() { const imgReducer = (state, action) => { switch (action.type) { case 'STACK_IMAGES': return { ...state, images: state.images.concat(action.images) } case 'FETCHING_IMAGES': return { ...state, fetching: action.fetching } default: return state; } } const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true}) // next code block goes here }
En primer lugar, definimos una función reductora, imgReducer
. Este reductor maneja dos acciones.
- La acción
STACK_IMAGES
concatena la matriz deimages
. - La acción
FETCHING_IMAGES
alterna el valor de la variable defetching
entretrue
yfalse
.
El siguiente paso es conectar este reductor a un gancho useReducer
. Una vez hecho esto, obtenemos dos cosas:
-
imgData
, que contiene dos variables:images
es la matriz de objetos de imagen.fetching
es un valor booleano que nos dice si la llamada a la API está en curso o no. -
imgDispatch
, que es una función para actualizar el objeto reductor.
Puede obtener más información sobre el gancho useReducer
en la documentación de React.
La siguiente parte del código es donde hacemos la llamada a la API. Pegue el siguiente código debajo del bloque de código anterior en App.js
// make API calls useEffect(() => { imgDispatch({ type: 'FETCHING_IMAGES', fetching: true }) fetch('https://picsum.photos/v2/list?page=0&limit=10') .then(data => data.json()) .then(images => { imgDispatch({ type: 'STACK_IMAGES', images }) imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) }) .catch(e => { // handle error imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) return e }) }, [ imgDispatch ]) // next code block goes here
Dentro del gancho useEffect
, hacemos una llamada al extremo de la API con la API de fetch
. Luego actualizamos la matriz de imágenes con el resultado de la llamada a la API enviando la acción STACK_IMAGES
. También enviamos la acción FETCHING_IMAGES
una vez que se completa la llamada a la API.
El siguiente bloque de código define el valor de retorno de la función. Ingrese el siguiente código después del gancho useEffect
.
return ( <div className=""> <nav className="navbar bg-light"> <div className="container"> <a className="navbar-brand" href="/#"> <h2>Infinite scroll + image lazy loading</h2> </a> </div> </navv <div id='images' className="container"> <div className="row"> {imgData.images.map((image, index) => { const { author, download_url } = image return ( <div key={index} className="card"> <div className="card-body "> <img alt={author} className="card-img-top" src={download_url} /> </div> <div className="card-footer"> <p className="card-text text-center text-capitalize text-primary">Shot by: {author}</p> </div> </div> ) })} </div> </div> </div> );
Para mostrar las imágenes, mapeamos sobre la matriz de imágenes en el objeto imgData
.
Ahora inicie la aplicación y vea la página en el navegador. Debería ver las imágenes bien mostradas en una cuadrícula receptiva.
El último bit es exportar el componente de la aplicación.
export default App;
La rama correspondiente en este punto es 01-make-api-calls.
Ahora ampliemos esto mostrando más imágenes a medida que se desplaza la página.
Implementando desplazamiento infinito
Nuestro objetivo es presentar más imágenes a medida que avanza la página. A partir de la URL del extremo de la API, https://picsum.photos/v2/list?page=0&limit=10
, sabemos que para obtener un nuevo conjunto de fotos, solo necesitamos incrementar el valor de page
. También debemos hacer esto cuando nos hayamos quedado sin imágenes para mostrar. Para nuestro propósito aquí, sabremos que nos hemos quedado sin imágenes cuando lleguemos al final de la página. Es hora de ver cómo la API Intersection Observer nos ayuda a lograrlo.
Abra src/App.js
y cree un nuevo reductor, pageReducer
, debajo de imgReducer
.
// App.js const imgReducer = (state, action) => { ... } const pageReducer = (state, action) => { switch (action.type) { case 'ADVANCE_PAGE': return { ...state, page: state.page + 1 } default: return state; } } const [ pager, pagerDispatch ] = useReducer(pageReducer, { page: 0 })
Definimos un solo tipo de acción. Cada vez que se activa la acción ADVANCE_PAGE
, el valor de la page
se incrementa en 1.
Actualice la URL en la función de fetch
para aceptar números de página dinámicamente como se muestra a continuación.
fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)
Agregue pager.page
a la matriz de dependencias junto con imgData
. Hacer esto asegura que la llamada a la API se ejecutará cada vez que cambie pager.page
.
useEffect(() => { ... }, [ imgDispatch, pager.page ])
Después del useEffect
para la llamada API, ingrese el código a continuación. Actualice su línea de importación también.
// App.js import React, { useEffect, useReducer, useCallback, useRef } from 'react'; useEffect(() => { ... }, [ imgDispatch, pager.page ]) // implement infinite scrolling with intersection observer let bottomBoundaryRef = useRef(null); const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { pagerDispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [pagerDispatch] ); useEffect(() => { if (bottomBoundaryRef.current) { scrollObserver(bottomBoundaryRef.current); } }, [scrollObserver, bottomBoundaryRef]);
Definimos una variable bottomBoundaryRef
y establecemos su valor en useRef(null)
. useRef
permite que las variables conserven sus valores en las renderizaciones de los componentes, es decir, el valor actual de la variable persiste cuando el componente que las contiene se vuelve a renderizar. La única forma de cambiar su valor es reasignando la propiedad .current
en esa variable.
En nuestro caso, bottomBoundaryRef.current
comienza con un valor null
. A medida que avanza el ciclo de representación de la página, establecemos que su propiedad actual sea el nodo <div id='page-bottom-boundary'>
.
Usamos la instrucción de asignación ref={bottomBoundaryRef}
para decirle a React que configure bottomBoundaryRef.current
como el div donde se declara esta asignación.
Por lo tanto,
bottomBoundaryRef.current = null
al final del ciclo de renderizado, se convierte en:
bottomBoundaryRef.current = <div></div>
Veremos dónde se realiza esta asignación en un minuto.
A continuación, definimos una función scrollObserver
, en la que establecer el observador. Esta función acepta un nodo DOM
para observar. El punto principal a tener en cuenta aquí es que cada vez que llegamos a la intersección bajo observación, despachamos la acción ADVANCE_PAGE
. El efecto es incrementar el valor de pager.page
en 1. Una vez que esto sucede, se vuelve a ejecutar el useEffect
que lo tiene como dependencia. Esta repetición, a su vez, invoca la llamada de búsqueda con el nuevo número de página.
La procesión del evento se ve así.
Golpee la intersección bajo observación → llame a la acciónADVANCE_PAGE
→ incremente el valor depager.page
en 1 →useEffect
hook para ejecutar llamadas defetch
→ ejecute la llamada de búsqueda → las imágenes devueltas se concatenan en la matriz deimages
.
Invocamos scrollObserver
en un useEffect
para que la función se ejecute solo cuando cambie cualquiera de las dependencias del enlace. Si no llamamos a la función dentro de un useEffect
, la función se ejecutaría en cada representación de página.
Recuerde que bottomBoundaryRef.current
se refiere a <div id="page-bottom-boundary" style="border: 1px solid red;"></div>
. Verificamos que su valor no sea nulo antes de pasarlo a scrollObserver
. De lo contrario, el constructor IntersectionObserver
devolvería un error.
Debido a que usamos scrollObserver
en un gancho useEffect
, tenemos que envolverlo en un gancho useCallback
para evitar que los componentes se vuelvan a renderizar interminablemente. Puede obtener más información sobre useCallback en los documentos de React.
Ingrese el siguiente código después del <div id='images'>
.
// App.js <div id='image'> ... </div> {imgData.fetching && ( <div className="text-center bg-secondary m-auto p-3"> <p className="m-0 text-white">Getting images</p> </div> )} <div id='page-bottom-boundary' style={{ border: '1px solid red' }} ref={bottomBoundaryRef}></div>
Cuando se inicia la llamada a la API, establecemos la fetching
en true
y el texto Obtener imágenes se vuelve visible. Tan pronto como finaliza, establecemos la fetching
en false
y el texto se oculta. También podríamos activar la llamada a la API antes de alcanzar el límite exactamente estableciendo un threshold
diferente en el objeto de opciones del constructor. La línea roja al final nos permite ver exactamente cuándo llegamos al límite de la página.
La rama correspondiente en este punto es 02-infinite-scroll.
Ahora implementaremos la carga diferida de imágenes.
Implementación de carga diferida de imágenes
Si inspecciona la pestaña de la red a medida que se desplaza hacia abajo, verá que tan pronto como llegue a la línea roja (el límite inferior), se produce la llamada a la API y todas las imágenes comienzan a cargarse incluso cuando no las ha visto. ellos. Hay una variedad de razones por las que esto podría no ser un comportamiento deseable. Es posible que queramos guardar las llamadas de red hasta que el usuario quiera ver una imagen. En tal caso, podríamos optar por cargar las imágenes de forma diferida, es decir, no cargaremos una imagen hasta que se desplace a la vista.
Abre src/App.js
. Justo debajo de las funciones de desplazamiento infinito, ingrese el siguiente código.
// App.js // lazy loads images with intersection observer // only swap out the image source if the new url exists const imagesRef = useRef(null); const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); useEffect(() => { imagesRef.current = document.querySelectorAll('.card-img-top'); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgData.images]);
Al igual que con scrollObserver
, definimos una función, imgObserver
, que acepta un nodo para observar. Cuando la página llega a una intersección, según lo determinado por en.intersectionRatio > 0
, intercambiamos la fuente de la imagen en el elemento. Tenga en cuenta que primero verificamos si existe la nueva fuente de imagen antes de realizar el intercambio. Al igual que con la función scrollObserver
, envolvemos imgObserver en un useCallback
para evitar que el componente se vuelva a renderizar sin terminar.
También tenga en cuenta que dejamos de observar un elemento img
una vez que terminamos con la sustitución. Hacemos esto con el método de unobserve
.
En el siguiente useEffect
, capturamos todas las imágenes con una clase de .card-img-top
en la página con document.querySelectorAll
. Luego iteramos sobre cada imagen y colocamos un observador en ella.
Tenga en cuenta que agregamos imgData.images
como una dependencia del useEffect
. Cuando esto cambia, activa el gancho useEffect
y, a su vez, se llama a imgObserver
con cada elemento <img className='card-img-top'>
.
Actualice el elemento <img className='card-img-top'/>
como se muestra a continuación.
<img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />
Establecemos una fuente predeterminada para cada elemento <img className='card-img-top'/>
y almacenamos la imagen que queremos mostrar en la propiedad data-src
. La imagen por defecto suele tener un tamaño pequeño para que descarguemos lo menos posible. Cuando aparece el elemento <img/>
, el valor de la propiedad data-src
reemplaza la imagen predeterminada.
En la imagen a continuación, vemos que la imagen predeterminada del faro aún se muestra en algunos de los espacios.
La rama correspondiente en este punto es 03-lazy-loading.
Veamos ahora cómo podemos abstraer todas estas funciones para que sean reutilizables.
Recuperación abstracta, desplazamiento infinito y carga diferida en ganchos personalizados
Hemos implementado con éxito la recuperación, el desplazamiento infinito y la carga diferida de imágenes. Es posible que tengamos otro componente en nuestra aplicación que necesite una funcionalidad similar. En ese caso, podríamos abstraer y reutilizar estas funciones. Todo lo que tenemos que hacer es moverlos dentro de un archivo separado e importarlos donde los necesitemos. Queremos convertirlos en ganchos personalizados.
La documentación de React define un gancho personalizado como una función de JavaScript cuyo nombre comienza con "use"
y que puede llamar a otros ganchos. En nuestro caso, queremos crear tres ganchos, useFetch
, useInfiniteScroll
, useLazyLoading
.
Cree un archivo dentro de la carpeta src/
. customHooks.js
y pegue el siguiente código dentro.
// customHooks.js import { useEffect, useCallback, useRef } from 'react'; // make API calls and pass the returned data via dispatch export const useFetch = (data, dispatch) => { useEffect(() => { dispatch({ type: 'FETCHING_IMAGES', fetching: true }); fetch(`https://picsum.photos/v2/list?page=${data.page}&limit=10`) .then(data => data.json()) .then(images => { dispatch({ type: 'STACK_IMAGES', images }); dispatch({ type: 'FETCHING_IMAGES', fetching: false }); }) .catch(e => { dispatch({ type: 'FETCHING_IMAGES', fetching: false }); return e; }) }, [dispatch, data.page]) } // next code block here
El hook useFetch
acepta una función de envío y un objeto de datos. La función de envío pasa los datos de la llamada API al componente de la App
, mientras que el objeto de datos nos permite actualizar la URL del punto final de la API.
// infinite scrolling with intersection observer export const useInfiniteScroll = (scrollRef, dispatch) => { const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { dispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [dispatch] ); useEffect(() => { if (scrollRef.current) { scrollObserver(scrollRef.current); } }, [scrollObserver, scrollRef]); } // next code block here
El gancho useInfiniteScroll
acepta un scrollRef
y una función de dispatch
. scrollRef
nos ayuda a configurar el observador, como ya se discutió en la sección donde lo implementamos. La función de envío proporciona una forma de desencadenar una acción que actualiza el número de página en la URL del punto final de la API.
// lazy load images with intersection observer export const useLazyLoading = (imgSelector, items) => { const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); const imagesRef = useRef(null); useEffect(() => { imagesRef.current = document.querySelectorAll(imgSelector); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgSelector, items]) }
El gancho useLazyLoading
recibe un selector y una matriz. El selector se utiliza para encontrar las imágenes. Cualquier cambio en la matriz activa el gancho useEffect
que configura el observador en cada imagen.
Podemos ver que son las mismas funciones que tenemos en src/App.js
que hemos extraído a un nuevo archivo. Lo bueno ahora es que podemos pasar argumentos dinámicamente. Ahora usemos estos ganchos personalizados en el componente de la aplicación.
Abra src/App.js
. Importe los ganchos personalizados y elimine las funciones que definimos para obtener datos, desplazamiento infinito y carga diferida de imágenes. Deja los reductores y las secciones donde hacemos uso de useReducer
. Pegue el siguiente código.
// App.js // import custom hooks import { useFetch, useInfiniteScroll, useLazyLoading } from './customHooks' const imgReducer = (state, action) => { ... } // retain this const pageReducer = (state, action) => { ... } // retain this const [pager, pagerDispatch] = useReducer(pageReducer, { page: 0 }) // retain this const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true }) // retain this let bottomBoundaryRef = useRef(null); useFetch(pager, imgDispatch); useLazyLoading('.card-img-top', imgData.images) useInfiniteScroll(bottomBoundaryRef, pagerDispatch); // retain the return block return ( ... )
Ya hemos hablado de bottomBoundaryRef
en la sección de scroll infinito. Pasamos el objeto pager
y la función imgDispatch
a useFetch
. useLazyLoading
acepta el nombre de clase .card-img-top
. Tenga en cuenta el .
incluido en el nombre de la clase. Al hacer esto, no necesitamos especificarlo document.querySelectorAll
. useInfiniteScroll
acepta tanto una referencia como la función de envío para incrementar el valor de la page
.
La rama correspondiente en este punto es 04-custom-hooks.
Conclusión
HTML está mejorando en proporcionar buenas API para implementar características geniales. En esta publicación, hemos visto lo fácil que es usar el observador de intersección en un componente funcional de React. En el proceso, aprendimos cómo usar algunos de los ganchos de React y cómo escribir nuestros propios ganchos.
Recursos
- "Desplazamiento infinito + carga diferida de imágenes", Orji Chidi Matthew, GitHub
- “¿Desplazamiento infinito, paginación o botones de “Cargar más”? Hallazgos de usabilidad en el comercio electrónico”, Christian Holst, Smashing Magazine
- "Lorem Picsum", David Marby y Nijiko Yonskai
- "IntersectionObserver's Coming Into View", Surma, Web Fundamentals
- ¿Puedo usar...
IntersectionObserver
- "Intersection Observer API", documentos web de MDN
- "Componentes y accesorios", React
- “
useCallback
,” Reaccionar - “
useReducer
,” Reaccionar