Implémentation du défilement infini et du chargement paresseux d'images dans React
Publié: 2022-03-10HTML
Intersection Observer
pour implémenter le défilement infini et le chargement différé d'images dans un composant fonctionnel React. Au cours du processus, nous apprendrons à utiliser certains crochets de React et à créer des crochets personnalisés.Si vous cherchiez une alternative à la pagination, le défilement infini est une bonne considération. Dans cet article, nous allons explorer quelques cas d'utilisation de l'API Intersection Observer dans le contexte d'un composant fonctionnel React. Le lecteur doit posséder une connaissance pratique des composants fonctionnels de React. Une certaine familiarité avec les crochets React sera bénéfique mais pas obligatoire, car nous en examinerons quelques-uns.
Notre objectif est qu'à la fin de cet article, nous aurons implémenté le défilement infini et le chargement paresseux d'images à l'aide d'une API HTML native. Nous aurions également appris quelques choses supplémentaires sur React Hooks. Avec cela, vous pouvez implémenter un défilement infini et un chargement paresseux d'image dans votre application React si nécessaire.
Commençons.
Créer des cartes avec React et Leaflet
Saisir des informations à partir d'un fichier CSV ou JSON n'est pas seulement compliqué, mais aussi fastidieux. Représenter les mêmes données sous forme d'aide visuelle est plus simple. Shajia Abidi explique la puissance d'un outil Leaflet et comment de nombreux types de cartes différents peuvent être créés. Lire un article connexe →
L'API Intersection Observer
Selon les documents MDN, "l'API Intersection Observer fournit un moyen d'observer de manière asynchrone les changements dans l'intersection d'un élément cible avec un élément ancêtre ou avec la fenêtre d'affichage d'un document de niveau supérieur".
Cette API nous permet d'implémenter des fonctionnalités intéressantes telles que le défilement infini et le chargement paresseux d'images. L'observateur d'intersection est créé en appelant son constructeur et en lui transmettant un rappel et un objet d'options. Le rappel est invoqué chaque fois qu'un élément, appelé target
, croise soit la fenêtre d'affichage de l'appareil, soit un élément spécifié, appelé root
. Nous pouvons spécifier une racine personnalisée dans l'argument options ou utiliser la valeur par défaut.
let observer = new IntersectionObserver(callback, options);
L'API est simple à utiliser. Un exemple typique ressemble à ceci :
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
est une liste d'objets IntersectionObserverEntry
. L'objet IntersectionObserverEntry
décrit un changement d'intersection pour un élément cible observé. Notez que le rappel ne doit pas gérer de tâche chronophage car il s'exécute sur le thread principal.
L'API Intersection Observer bénéficie actuellement d'un large support de navigateur, comme indiqué sur caniuse.
Vous pouvez en savoir plus sur l'API dans les liens fournis dans la section des ressources.
Voyons maintenant comment utiliser cette API dans une véritable application React. La version finale de notre application sera une page d'images qui défile à l'infini et dont chaque image sera chargée paresseusement.
Faire des appels d'API avec le useEffect
Pour commencer, clonez le projet de démarrage à partir de cette URL. Il a une configuration minimale et quelques styles définis. J'ai également ajouté un lien vers le CSS de Bootstrap
dans le fichier public/index.html
car j'utiliserai ses classes pour le style.
N'hésitez pas à créer un nouveau projet si vous le souhaitez. Assurez-vous que le gestionnaire de paquets de yarn
est installé si vous souhaitez suivre le référentiel. Vous pouvez trouver les instructions d'installation pour votre système d'exploitation spécifique ici.
Pour ce didacticiel, nous allons récupérer des images à partir d'une API publique et les afficher sur la page. Nous utiliserons les API Lorem Picsum.
Pour ce didacticiel, nous utiliserons le point de terminaison, https://picsum.photos/v2/list?page=0&limit=10
, qui renvoie un tableau d'objets image. Pour obtenir les dix images suivantes, nous changeons la valeur de page en 1, puis 2, et ainsi de suite.
Nous allons maintenant construire le composant App pièce par pièce.
Ouvrez src/App.js
et entrez le code suivant.
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 }
Tout d'abord, nous définissons une fonction de réduction, imgReducer
. Ce réducteur gère deux actions.
- L'action
STACK_IMAGES
concatène le tableau d'images
. - L'action
FETCHING_IMAGES
basculer la valeur de la variable defetching
entretrue
etfalse
.
L'étape suivante consiste à câbler ce réducteur à un crochet useReducer
. Une fois cela fait, on récupère deux choses :
-
imgData
, qui contient deux variables :images
est le tableau d'objets image.fetching
est un booléen qui nous indique si l'appel API est en cours ou non. -
imgDispatch
, qui est une fonction de mise à jour de l'objet réducteur.
Vous pouvez en savoir plus sur le crochet useReducer
dans la documentation de React.
La partie suivante du code est l'endroit où nous effectuons l'appel d'API. Collez le code suivant sous le bloc de code précédent dans 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
Dans le crochet useEffect
, nous appelons le point de terminaison de l'API avec l'API fetch
. Nous mettons ensuite à jour le tableau d'images avec le résultat de l'appel API en envoyant l'action STACK_IMAGES
. Nous envoyons également l'action FETCHING_IMAGES
une fois l'appel d'API terminé.
Le bloc de code suivant définit la valeur de retour de la fonction. Entrez le code suivant après le crochet 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> );
Pour afficher les images, nous mappons sur le tableau d'images dans l'objet imgData
.
Maintenant, démarrez l'application et affichez la page dans le navigateur. Vous devriez voir les images bien affichées dans une grille réactive.
Le dernier bit consiste à exporter le composant App.
export default App;
La branche correspondante à ce stade est 01-make-api-calls.
Étendons maintenant cela en affichant plus d'images au fur et à mesure que la page défile.
Implémentation du défilement infini
Notre objectif est de présenter plus de photos au fur et à mesure que la page défile. À partir de l'URL du point de terminaison de l'API, https://picsum.photos/v2/list?page=0&limit=10
, nous savons que pour obtenir un nouvel ensemble de photos, nous n'avons qu'à incrémenter la valeur de page
. Nous devons également le faire lorsque nous n'avons plus d'images à montrer. Pour notre propos ici, nous saurons que nous n'avons plus d'images lorsque nous atteindrons le bas de la page. Il est temps de voir comment l'API Intersection Observer nous aide à y parvenir.
Ouvrez src/App.js
et créez un nouveau réducteur, pageReducer
, sous 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 })
Nous définissons un seul type d'action. A chaque déclenchement de l'action ADVANCE_PAGE
, la valeur de page
est incrémentée de 1.
Mettez à jour l'URL dans la fonction de fetch
pour accepter les numéros de page dynamiquement, comme indiqué ci-dessous.
fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)
Ajoutez pager.page
au tableau de dépendances aux côtés de imgData
. Cela garantit que l'appel d'API s'exécutera chaque fois que pager.page
changera.
useEffect(() => { ... }, [ imgDispatch, pager.page ])
Après le crochet useEffect
pour l'appel d'API, entrez le code ci-dessous. Mettez également à jour votre ligne d'importation.
// 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]);
Nous définissons une variable bottomBoundaryRef
et définissons sa valeur sur useRef(null)
. useRef
permet aux variables de conserver leurs valeurs à travers les rendus de composants, c'est-à-dire que la valeur actuelle de la variable persiste lorsque le composant contenant se restitue. La seule façon de changer sa valeur est de réassigner la propriété .current
sur cette variable.
Dans notre cas, bottomBoundaryRef.current
commence par une valeur null
. Au fur et à mesure que le cycle de rendu de la page se poursuit, nous définissons sa propriété actuelle sur le nœud <div id='page-bottom-boundary'>
.
Nous utilisons l'instruction d'affectation ref={bottomBoundaryRef}
pour indiquer à React de définir bottomBoundaryRef.current
comme étant la div où cette affectation est déclarée.
Ainsi,
bottomBoundaryRef.current = null
à la fin du cycle de rendu, devient :
bottomBoundaryRef.current = <div></div>
Nous verrons où cette affectation est effectuée dans une minute.
Ensuite, nous définissons une fonction scrollObserver
, dans laquelle définir l'observateur. Cette fonction accepte un nœud DOM
à observer. Le point principal à noter ici est que chaque fois que nous atteignons l'intersection sous observation, nous envoyons l'action ADVANCE_PAGE
. L'effet est d'incrémenter la valeur de pager.page
de 1. Une fois que cela se produit, le crochet useEffect
qui l'a comme dépendance est réexécuté. Cette réexécution, à son tour, invoque l'appel de récupération avec le nouveau numéro de page.
Le cortège de l'événement ressemble à ceci.
Frappez l'intersection sous observation → appelez l'actionADVANCE_PAGE
→ incrémentez la valeur depager.page
de 1 → le crochetuseEffect
pour l'appel de récupération s'exécute → l'appel defetch
est exécuté → les images renvoyées sont concaténées au tableau d'images
.
Nous scrollObserver
dans un crochet useEffect
afin que la fonction ne s'exécute que lorsque l'une des dépendances du crochet change. Si nous n'appelions pas la fonction à l'intérieur d'un crochet useEffect
, la fonction s'exécuterait sur chaque rendu de page.
Rappelez-vous que bottomBoundaryRef.current
fait référence à <div id="page-bottom-boundary" style="border: 1px solid red;"></div>
. Nous vérifions que sa valeur n'est pas nulle avant de la passer à scrollObserver
. Sinon, le constructeur IntersectionObserver
renverrait une erreur.
Parce que nous avons utilisé scrollObserver
dans un crochet useEffect
, nous devons l'envelopper dans un crochet useCallback
pour empêcher les rendus de composants sans fin. Vous pouvez en savoir plus sur useCallback dans les documents React.
Entrez le code ci-dessous après la <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>
Lorsque l'appel d'API démarre, nous définissons fetching
sur true
et le texte Obtenir des images devient visible. Dès qu'il se termine, nous définissons la fetching
sur false
et le texte est masqué. Nous pourrions également déclencher l'appel d'API avant d'atteindre exactement la limite en définissant un threshold
différent dans l'objet d'options du constructeur. La ligne rouge à la fin nous permet de voir exactement quand nous atteignons la limite de la page.
La branche correspondante à ce point est 02-infinite-scroll.
Nous allons maintenant implémenter le chargement différé des images.
Mise en œuvre du chargement différé d'images
Si vous inspectez l'onglet réseau lorsque vous faites défiler vers le bas, vous verrez que dès que vous atteignez la ligne rouge (la limite inférieure), l'appel API se produit et toutes les images commencent à se charger même lorsque vous n'avez pas pu visualiser leur. Il existe diverses raisons pour lesquelles ce comportement peut ne pas être souhaitable. Nous pouvons vouloir enregistrer les appels réseau jusqu'à ce que l'utilisateur veuille voir une image. Dans un tel cas, nous pourrions opter pour un chargement paresseux des images, c'est-à-dire que nous ne chargerons pas une image tant qu'elle ne défilera pas dans la vue.
Ouvrez src/App.js
. Juste en dessous des fonctions de défilement infini, entrez le code suivant.
// 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]);
Comme pour scrollObserver
, nous définissons une fonction, imgObserver
, qui accepte un nœud à observer. Lorsque la page atteint une intersection, comme déterminé par en.intersectionRatio > 0
, nous échangeons la source de l'image sur l'élément. Notez que nous vérifions d'abord si la nouvelle source d'image existe avant de procéder à l'échange. Comme avec la fonction scrollObserver
, nous encapsulons imgObserver dans un hook useCallback
pour empêcher le nouveau rendu des composants sans fin.
Notez également que nous arrêtons d'observer un élément img
une fois que nous avons terminé la substitution. Nous le faisons avec la méthode unobserve
.
Dans le crochet useEffect
suivant, nous récupérons toutes les images avec une classe de .card-img-top
sur la page avec document.querySelectorAll
. Ensuite, nous parcourons chaque image et définissons un observateur dessus.
Notez que nous avons ajouté imgData.images
en tant que dépendance du crochet useEffect
. Lorsque cela change, il déclenche le crochet useEffect
et à son tour imgObserver
est appelé avec chaque <img className='card-img-top'>
.
Mettez à jour l' <img className='card-img-top'/>
comme indiqué ci-dessous.
<img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />
Nous définissons une source par défaut pour chaque <img className='card-img-top'/>
et stockons l'image que nous voulons afficher sur la propriété data-src
. L'image par défaut a généralement une petite taille afin que nous téléchargions le moins possible. Lorsque l'élément <img/>
apparaît, la valeur de la propriété data-src
remplace l'image par défaut.
Dans l'image ci-dessous, nous voyons l'image du phare par défaut toujours affichée dans certains espaces.
La branche correspondante à ce stade est 03-lazy-loading.
Voyons maintenant comment nous pouvons abstraire toutes ces fonctions afin qu'elles soient réutilisables.
Extraction abstraite, défilement infini et chargement paresseux dans des crochets personnalisés
Nous avons implémenté avec succès la récupération, le défilement infini et le chargement paresseux d'images. Nous pourrions avoir un autre composant dans notre application qui nécessite des fonctionnalités similaires. Dans ce cas, nous pourrions abstraire et réutiliser ces fonctions. Tout ce que nous avons à faire est de les déplacer dans un fichier séparé et de les importer là où nous en avons besoin. Nous voulons les transformer en crochets personnalisés.
La documentation de React définit un Custom Hook comme une fonction JavaScript dont le nom commence par "use"
et qui peut appeler d'autres hooks. Dans notre cas, nous voulons créer trois hooks, useFetch
, useInfiniteScroll
, useLazyLoading
.
Créez un fichier dans le dossier src/
. Nommez-le customHooks.js
et collez le code ci-dessous à l'intérieur.
// 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
Le crochet useFetch
accepte une fonction de répartition et un objet de données. La fonction de répartition transmet les données de l'appel d'API au composant App
, tandis que l'objet de données nous permet de mettre à jour l'URL du point de terminaison de l'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
Le hook useInfiniteScroll
accepte un scrollRef
et une fonction dispatch
. Le scrollRef
nous aide à configurer l'observateur, comme déjà discuté dans la section où nous l'avons implémenté. La fonction de répartition permet de déclencher une action qui met à jour le numéro de page dans l'URL du point de terminaison de l'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]) }
Le crochet useLazyLoading
reçoit un sélecteur et un tableau. Le sélecteur est utilisé pour trouver les images. Tout changement dans le tableau déclenche le hook useEffect
qui configure l'observateur sur chaque image.
Nous pouvons voir que ce sont les mêmes fonctions que nous avons dans src/App.js
que nous avons extraites dans un nouveau fichier. La bonne chose maintenant est que nous pouvons passer des arguments dynamiquement. Utilisons maintenant ces hooks personnalisés dans le composant App.
Ouvrez src/App.js
. Importez les crochets personnalisés et supprimez les fonctions que nous avons définies pour la récupération des données, le défilement infini et le chargement différé des images. Laissez les réducteurs et les sections où nous utilisons useReducer
. Collez le code ci-dessous.
// 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 ( ... )
Nous avons déjà parlé de bottomBoundaryRef
dans la section sur le défilement infini. Nous passons l'objet pager
et la fonction imgDispatch
à useFetch
. useLazyLoading
accepte le nom de classe .card-img-top
. Notez le .
inclus dans le nom de la classe. En faisant cela, nous n'avons pas besoin de le spécifier document.querySelectorAll
. useInfiniteScroll
accepte à la fois une référence et la fonction dispatch pour incrémenter la valeur de page
.
La branche correspondante à ce stade est 04-custom-hooks.
Conclusion
HTML s'améliore pour fournir de belles API pour implémenter des fonctionnalités intéressantes. Dans cet article, nous avons vu à quel point il est facile d'utiliser l'observateur d'intersection dans un composant fonctionnel React. Au cours du processus, nous avons appris à utiliser certains crochets de React et à écrire nos propres crochets.
Ressources
- "Défilement infini + chargement paresseux d'images", Orji Chidi Matthew, GitHub
- "Défilement infini, pagination ou boutons "Charger plus" ? Résultats d'utilisabilité dans le commerce électronique », Christian Holst, Smashing Magazine
- « Lorem Picsum », David Marby et Nijiko Yonskai
- "IntersectionObserver's Coming Into View", Surma, Web Fundamentals
- Puis-je utiliser…
IntersectionObserver
- « API Intersection Observer », documentation Web MDN
- "Composants et accessoires", React
- «
useCallback
», Réagissez - «
useReducer
», réagir