Implementazione di scorrimento infinito e caricamento pigro dell'immagine in risposta
Pubblicato: 2022-03-10HTML
Intersection Observer
per implementare lo scorrimento infinito e il caricamento lento delle immagini in un componente funzionale di React. Nel processo, impareremo come utilizzare alcuni hook di React e come creare Hook personalizzati.Se stai cercando un'alternativa all'impaginazione, lo scorrimento infinito è una buona considerazione. In questo articolo, esploreremo alcuni casi d'uso per l'API Intersection Observer nel contesto di un componente funzionale React. Il lettore deve possedere una conoscenza pratica dei componenti funzionali di React. Una certa familiarità con gli hook React sarà utile ma non richiesta, dato che daremo un'occhiata ad alcuni.
Il nostro obiettivo è che alla fine di questo articolo avremo implementato lo scorrimento infinito e il caricamento lento delle immagini utilizzando un'API HTML nativa. Avremmo anche imparato alcune cose in più su React Hooks. Con ciò puoi essere in grado di implementare lo scorrimento infinito e il caricamento lento delle immagini nella tua applicazione React, ove necessario.
Iniziamo.
Creazione di mappe con React e Leaflet
Afferrare le informazioni da un file CSV o JSON non è solo complicato, ma è anche noioso. Rappresentare gli stessi dati sotto forma di ausilio visivo è più semplice. Shajia Abidi spiega quanto sia potente uno strumento Leaflet e come è possibile creare diversi tipi di mappe. Leggi un articolo correlato →
L'API dell'osservatore di intersezione
Secondo i documenti MDN, "l'API Intersection Observer fornisce un modo per osservare in modo asincrono i cambiamenti nell'intersezione di un elemento target con un elemento antenato o con il viewport di un documento di primo livello".
Questa API ci consente di implementare funzionalità interessanti come lo scorrimento infinito e il caricamento lento delle immagini. L'osservatore di intersezione viene creato chiamando il suo costruttore e passandogli un callback e un oggetto opzioni. Il callback viene invocato ogni volta che un elemento, chiamato target
, interseca la finestra del dispositivo o un elemento specificato, chiamato root
. Possiamo specificare una radice personalizzata nell'argomento delle opzioni o utilizzare il valore predefinito.
let observer = new IntersectionObserver(callback, options);
L'API è semplice da usare. Un tipico esempio si presenta così:
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
è un elenco di oggetti IntersectionObserverEntry
. L'oggetto IntersectionObserverEntry
descrive una modifica dell'intersezione per un elemento di destinazione osservato. Si noti che il callback non dovrebbe gestire attività che richiedono tempo poiché viene eseguito sul thread principale.
L'API di Intersection Observer attualmente gode di un ampio supporto per i browser, come mostrato su caniuse.
Puoi leggere ulteriori informazioni sull'API nei collegamenti forniti nella sezione risorse.
Vediamo ora come utilizzare questa API in una vera app React. La versione finale della nostra app sarà una pagina di immagini che scorre all'infinito e ogni immagine verrà caricata pigramente.
Effettuare chiamate API con l'hook useEffect
Per iniziare, clona il progetto iniziale da questo URL. Ha una configurazione minima e alcuni stili definiti. Ho anche aggiunto un collegamento al CSS di Bootstrap
nel file public/index.html
poiché userò le sue classi per lo stile.
Sentiti libero di creare un nuovo progetto, se lo desideri. Assicurati di avere installato il gestore di pacchetti yarn
se vuoi seguire il repository. Puoi trovare le istruzioni di installazione per il tuo sistema operativo specifico qui.
Per questo tutorial, prenderemo le immagini da un'API pubblica e le visualizzeremo sulla pagina. Utilizzeremo le API di Lorem Picsum.
Per questo tutorial, utilizzeremo l'endpoint, https://picsum.photos/v2/list?page=0&limit=10
, che restituisce un array di oggetti immagine. Per ottenere le prossime dieci immagini, cambiamo il valore della pagina in 1, quindi 2 e così via.
Ora costruiremo il componente dell'app pezzo per pezzo.
Apri src/App.js
e inserisci il codice seguente.
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 }
Innanzitutto, definiamo una funzione riduttore, imgReducer
. Questo riduttore gestisce due azioni.
- L'azione
STACK_IMAGES
concatena l'array diimages
. - L'azione
FETCHING_IMAGES
alterna il valore della variabile difetching
tratrue
efalse
.
Il prossimo passo è collegare questo riduttore a un gancio useReducer
. Fatto ciò, otteniamo due cose:
-
imgData
, che contiene due variabili:images
è l'array di oggetti immagine.fetching
è un valore booleano che ci dice se la chiamata API è in corso o meno. -
imgDispatch
, che è una funzione per aggiornare l'oggetto riduttore.
Puoi saperne di più useReducer
nella documentazione di React.
La parte successiva del codice è dove effettuiamo la chiamata API. Incolla il codice seguente sotto il blocco di codice precedente in 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
All'interno useEffect
, effettuiamo una chiamata all'endpoint API con fetch
API. Quindi aggiorniamo l'array di immagini con il risultato della chiamata API inviando l'azione STACK_IMAGES
. Inviamo anche l'azione FETCHING_IMAGES
una volta completata la chiamata API.
Il blocco di codice successivo definisce il valore di ritorno della funzione. Immettere il codice seguente dopo l'hook 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> );
Per visualizzare le immagini, eseguiamo il mapping sull'array di immagini nell'oggetto imgData
.
Ora avvia l'app e visualizza la pagina nel browser. Dovresti vedere le immagini ben visualizzate in una griglia reattiva.
L'ultimo bit consiste nell'esportare il componente App.
export default App;
Il ramo corrispondente a questo punto è 01-make-api-calls.
Ora estendiamo questo visualizzando più immagini mentre la pagina scorre.
Implementazione dello scorrimento infinito
Miriamo a presentare più immagini mentre la pagina scorre. Dall'URL dell'endpoint API, https://picsum.photos/v2/list?page=0&limit=10
, sappiamo che per ottenere un nuovo set di foto, dobbiamo solo aumentare il valore di page
. Dobbiamo farlo anche quando abbiamo esaurito le immagini da mostrare. Per il nostro scopo qui, sapremo di aver esaurito le immagini quando raggiungeremo la fine della pagina. È ora di vedere come l'Intersection Observer API ci aiuta a raggiungere questo obiettivo.
Apri src/App.js
e crea un nuovo riduttore, pageReducer
, sotto 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 })
Definiamo un solo tipo di azione. Ogni volta che viene attivata l'azione ADVANCE_PAGE
, il valore della page
viene incrementato di 1.
Aggiorna l'URL nella funzione di fetch
per accettare i numeri di pagina in modo dinamico come mostrato di seguito.
fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)
Aggiungi pager.page
di dipendenza insieme a imgData
. In questo modo si assicura che la chiamata API venga eseguita ogni volta che pager.page
cambia.
useEffect(() => { ... }, [ imgDispatch, pager.page ])
Dopo l'hook useEffect
per la chiamata API, inserisci il codice seguente. Aggiorna anche la tua riga di importazione.
// 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]);
Definiamo una variabile bottomBoundaryRef
e impostiamo il suo valore su useRef(null)
. useRef
consente alle variabili di preservare i loro valori tra i rendering dei componenti, ovvero il valore corrente della variabile persiste quando il componente contenitore viene riprodotto. L'unico modo per modificarne il valore è riassegnare la proprietà .current
su quella variabile.
Nel nostro caso, bottomBoundaryRef.current
inizia con un valore null
. Man mano che il ciclo di rendering della pagina procede, impostiamo la sua proprietà corrente sul nodo <div id='page-bottom-boundary'>
.
Usiamo l'istruzione di assegnazione ref={bottomBoundaryRef}
per dire a React di impostare bottomBoundaryRef.current
come div in cui viene dichiarata questa assegnazione.
Così,
bottomBoundaryRef.current = null
al termine del ciclo di rendering, diventa:
bottomBoundaryRef.current = <div></div>
Vedremo tra un minuto dove verrà svolto questo compito.
Successivamente, definiamo una funzione scrollObserver
, in cui impostare l'osservatore. Questa funzione accetta un nodo DOM
da osservare. Il punto principale da notare qui è che ogni volta che raggiungiamo l'intersezione sotto osservazione, inviamo l'azione ADVANCE_PAGE
. L'effetto consiste nell'incrementare il valore di pager.page
di 1. Una volta che ciò accade, l'hook useEffect
che lo ha come dipendenza viene rieseguito. Questa ripetizione, a sua volta, richiama la chiamata di recupero con il nuovo numero di pagina.
La processione dell'evento si presenta così.
Hit intersezione sotto osservazione → callADVANCE_PAGE
action → incrementa il valore dipager.page
di 1 →useEffect
hook per l'esecuzione della chiamata difetch
→ viene eseguita la chiamata di recupero → le immagini restituite sono concatenate all'array diimages
.
Invochiamo scrollObserver
in un hook useEffect
in modo che la funzione venga eseguita solo quando una delle dipendenze dell'hook cambia. Se non chiamassimo la funzione all'interno di un hook useEffect
, la funzione verrebbe eseguita su ogni rendering di pagina.
Ricordiamo che bottomBoundaryRef.current
si riferisce a <div id="page-bottom-boundary" style="border: 1px solid red;"></div>
. Verifichiamo che il suo valore non sia null prima di passarlo a scrollObserver
. In caso contrario, il costruttore IntersectionObserver
restituirebbe un errore.
Poiché abbiamo usato scrollObserver
in un hook useEffect
, dobbiamo racchiuderlo in un hook useCallback
per evitare che i componenti vengano visualizzati nuovamente. Puoi saperne di più su useCallback nei documenti React.
Inserisci il codice seguente dopo il <div id='images'>
div.
// 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>
Quando viene avviata la chiamata API, impostiamo il fetching
su true
e il testo Ottenere immagini diventa visibile. Non appena finisce, impostiamo il fetching
su false
e il testo viene nascosto. Potremmo anche attivare la chiamata API prima di raggiungere il limite esattamente impostando una threshold
diversa nell'oggetto delle opzioni del costruttore. La linea rossa alla fine ci consente di vedere esattamente quando raggiungiamo il limite della pagina.
Il ramo corrispondente a questo punto è 02-infinite-scroll.
Ora implementeremo il caricamento lento dell'immagine.
Implementazione del caricamento lento dell'immagine
Se ispezioni la scheda di rete mentre scorri verso il basso, vedrai che non appena colpisci la linea rossa (il limite inferiore), viene eseguita la chiamata API e tutte le immagini iniziano a caricarsi anche quando non hai avuto modo di visualizzarle loro. Ci sono una serie di ragioni per cui questo potrebbe non essere un comportamento desiderabile. Potremmo voler salvare le chiamate di rete fino a quando l'utente non desidera vedere un'immagine. In tal caso, potremmo optare per il caricamento delle immagini in modo pigro, ovvero non caricheremo un'immagine finché non verrà visualizzata.
Apri src/App.js
. Appena sotto le infinite funzioni di scorrimento, inserisci il codice seguente.
// 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]);
Come con scrollObserver
, definiamo una funzione, imgObserver
, che accetta un nodo da osservare. Quando la pagina raggiunge un'intersezione, come determinato da en.intersectionRatio > 0
, scambiamo l'origine dell'immagine sull'elemento. Si noti che prima di eseguire lo scambio controlliamo se la nuova sorgente dell'immagine esiste. Come con la funzione scrollObserver
, avvolgiamo imgObserver in un hook useCallback
per impedire il re-rendering continuo dei componenti.
Nota anche che smettiamo di osservare un elemento img
una volta terminata la sostituzione. Lo facciamo con il metodo unobserve
.
Nel seguente hook useEffect
, prendiamo tutte le immagini con una classe di .card-img-top
sulla pagina con document.querySelectorAll
. Quindi ripetiamo su ciascuna immagine e impostiamo un osservatore su di essa.
Nota che abbiamo aggiunto imgData.images
come dipendenza useEffect
. Quando questo cambia, attiva l'hook useEffect
e, a sua volta imgObserver
viene chiamato con ogni elemento <img className='card-img-top'>
.
Aggiorna l'elemento <img className='card-img-top'/>
come mostrato di seguito.
<img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />
Impostiamo una sorgente predefinita per ogni elemento <img className='card-img-top'/>
e memorizziamo l'immagine che vogliamo mostrare nella proprietà data-src
. L'immagine predefinita di solito ha una dimensione ridotta in modo da scaricare il meno possibile. Quando viene visualizzato l'elemento <img/>
, il valore della proprietà data-src
sostituisce l'immagine predefinita.
Nell'immagine qui sotto, vediamo l'immagine predefinita del faro che appare ancora in alcuni spazi.
Il ramo corrispondente a questo punto è 03-lazy-loading.
Vediamo ora come possiamo astrarre tutte queste funzioni in modo che siano riutilizzabili.
Recupero astratto, scorrimento infinito e caricamento pigro in hook personalizzati
Abbiamo implementato con successo il recupero, lo scorrimento infinito e il caricamento lento delle immagini. Potremmo avere un altro componente nella nostra applicazione che necessita di funzionalità simili. In tal caso, potremmo astrarre e riutilizzare queste funzioni. Tutto quello che dobbiamo fare è spostarli in un file separato e importarli dove ne abbiamo bisogno. Vogliamo trasformarli in ganci personalizzati.
La documentazione di React definisce un Custom Hook come una funzione JavaScript il cui nome inizia con "use"
e che può chiamare altri hook. Nel nostro caso, vogliamo creare tre hook, useFetch
, useInfiniteScroll
, useLazyLoading
.
Crea un file all'interno della cartella src/
. customHooks.js
e incolla il codice qui sotto all'interno.
// 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
L'hook useFetch
accetta una funzione di invio e un oggetto dati. La funzione di invio passa i dati dalla chiamata API al componente App
, mentre l'oggetto dati ci consente di aggiornare l'URL dell'endpoint 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
L'hook useInfiniteScroll
accetta uno scrollRef
e una funzione di dispatch
. Lo scrollRef
ci aiuta a impostare l'osservatore, come già discusso nella sezione in cui lo abbiamo implementato. La funzione di invio consente di attivare un'azione che aggiorna il numero di pagina nell'URL dell'endpoint 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]) }
L'hook useLazyLoading
riceve un selettore e una matrice. Il selettore serve per trovare le immagini. Qualsiasi modifica nell'array attiva l'hook useEffect
che imposta l'osservatore su ogni immagine.
Possiamo vedere che sono le stesse funzioni che abbiamo in src/App.js
che abbiamo estratto in un nuovo file. La cosa buona ora è che possiamo passare gli argomenti in modo dinamico. Usiamo ora questi hook personalizzati nel componente App.
Apri src/App.js
. Importa gli hook personalizzati ed elimina le funzioni che abbiamo definito per il recupero dei dati, lo scorrimento infinito e il caricamento lento delle immagini. Lascia i riduttori e le sezioni in cui utilizziamo useReducer
. Incolla il codice sottostante.
// 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 ( ... )
Abbiamo già parlato di bottomBoundaryRef
nella sezione sullo scroll infinito. Passiamo l'oggetto pager
e la funzione imgDispatch
a useFetch
. useLazyLoading
accetta il nome della classe .card-img-top
. Nota il .
incluso nel nome della classe. In questo modo, non è necessario specificarlo document.querySelectorAll
. useInfiniteScroll
accetta sia un ref che la funzione dispatch per incrementare il valore di page
.
Il ramo corrispondente a questo punto è 04-custom-hooks.
Conclusione
L'HTML sta migliorando nel fornire belle API per l'implementazione di funzionalità interessanti. In questo post, abbiamo visto quanto sia facile usare l'osservatore di intersezione in un componente funzionale React. Nel processo, abbiamo imparato come utilizzare alcuni hook di React e come scrivere i nostri hook.
Risorse
- "Scorrimento infinito + caricamento lento dell'immagine", Orji Chidi Matthew, GitHub
- Pulsanti "Scorrimento infinito, impaginazione o "Carica altro"? Risultati di usabilità nell'eCommerce", Christian Holst, Smashing Magazine
- "Lorem Picsum", David Marby e Nijiko Yonskai
- "L'osservatore di intersezione sta venendo in vista", Surma, Fondamenti di Web
- Posso usare...
IntersectionObserver
- "Intersection Observer API", documenti Web MDN
- "Componenti e oggetti di scena", Reagire
- "
useCallback
", Reagisci - “
useReducer
,” Reagire