Fermer

mars 16, 2020

Implémentation du défilement infini et du chargement paresseux des images dans React


À propos de l'auteur

Génial développeur frontend qui adore tout le codage. Je suis un amoureux de la musique chorale et je travaille pour la rendre plus accessible au monde, un téléchargement à la…
En savoir plus sur
Chidi

Dans ce didacticiel, nous allons apprendre à utiliser l'API HTML Intersection Observer pour implémenter le défilement infini et le chargement paresseux d'images dans un composant fonctionnel React. Dans le processus, nous apprendrons comment utiliser certains crochets de React et comment créer des crochets personnalisés.

Si vous avez cherché une alternative à la pagination, infinite scroll est une bonne considération. Dans cet article, nous allons explorer certains 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 React . Une certaine familiarité avec les crochets React sera bénéfique mais pas obligatoire, car nous allons en examiner quelques-uns.

Notre objectif est qu'à la fin de cet article, nous aurons implémenté un défilement infini et chargement paresseux d'image à l'aide d'une API HTML native. Nous aurions également appris un peu plus de choses sur React Hooks. Avec cela, vous pouvez implémenter un défilement infini et un chargement paresseux d'images dans votre application React si nécessaire.

Commençons.

Création de cartes avec React et Leaflet

La saisie d'informations à partir d'un fichier CSV ou JSON n'est pas pas seulement compliqué, mais c'est aussi fastidieux. Représenter les mêmes données sous forme d'aide visuelle est plus simple. Shajia Abidi explique à quel point un dépliant est puissant et comment de nombreux types de cartes peuvent être créés. Lire l'article →

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 un élément ancêtre ou niveau de vue du document ".

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 passant un rappel et un objet options. La fonction de rappel est invoquée chaque fois qu'un élément, appelé la cible coupe la fenêtre d'affichage du périphérique ou un élément spécifié, appelé la racine . 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 (entrées => {
    entries.forEach (entrée => {
      console.log (entrée)
      console.log (entry.isIntersecting) // renvoie true si la cible croise l'élément racine
    })
  },
  {
    // options par défaut
  }
);
laissez target = document.querySelector ('# targetId');
intObserver.observe (cible); // start observation 

entrées 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 longue car il s'exécute sur le thread principal.

L'API Intersection Observer bénéficie actuellement d'une large prise en charge du navigateur, comme illustré sur caniuse .

Navigateur Intersection Observer soutien. ( Grand aperçu )

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 aura chaque image chargée paresseusement.

Faire des appels API avec le useEffect Hook

Pour commencer, clonez le projet de démarrage 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 yarn est installé si vous souhaitez suivre le dépôt. Vous pouvez trouver les instructions d'installation de votre système d'exploitation spécifique ici .

Pour ce didacticiel, nous allons récupérer des images 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 d'image. Pour obtenir les dix images suivantes, nous changeons la valeur de la page en 1, puis 2, etc.

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} de 'react';

import './index.css';

fonction App () {
  const imgReducer = (état, action) => {
    commutateur (action.type) {
      cas 'STACK_IMAGES':
        return {... state, images: state.images.concat (action.images)}
      cas 'FETCHING_IMAGES':
        return {... state, fetching: action.fetching}
      défaut:
        état de retour;
    }
  }
  const [imgData, imgDispatch] = useReducer (imgReducer, {images: []extraction: vrai})
  // le bloc de code suivant va ici
} 

Premièrement, nous définissons une fonction réductrice, imgReducer . Ce réducteur gère deux actions.

  1. L'action STACK_IMAGES concatène le tableau images .
  2. FETCHING_IMAGES actionne la valeur de la variable fetching . entre vrai et faux .

L'étape suivante consiste à câbler ce réducteur à un crochet useReducer . Une fois cela fait, nous récupérons deux choses:

  1. imgData qui contient deux variables: images est le tableau des objets d'image. la récupération est un booléen qui nous indique si l'appel API est en cours ou non.
  2. imgDispatch qui est une fonction de mise à jour de l'objet réducteur.

Vous pouvez en savoir plus sur le useReducer hook dans la documentation React .

La partie suivante du code est l'endroit où nous appelons l'API. Collez le code suivant sous le bloc de code précédent dans App.js .

 // passez des appels API
useEffect (() => {
  imgDispatch ({type: 'FETCHING_IMAGES', extraction: true})
  récupérer ('https://picsum.photos/v2/list?page=0&limit=10')
    .then (data => data.json ())
    .then (images => {
      imgDispatch ({type: 'STACK_IMAGES', images})
      imgDispatch ({type: 'FETCHING_IMAGES', extraction: false})
    })
    .catch (e => {
      // gère l'erreur
      imgDispatch ({type: 'FETCHING_IMAGES', extraction: false})
      retourner e
    })
}, [ imgDispatch ])

// le bloc de code suivant va ici 

Dans le crochet useEffect nous appelons le point de terminaison API avec fetch API. Nous mettons ensuite à jour le tableau d'images avec le résultat de l'appel d'API en envoyant l'action STACK_IMAGES . Nous envoyons également l'action FETCHING_IMAGES une fois l'appel 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 (
  
);

Pour afficher les images, nous mappons le tableau d'images dans l'objet imgData .

Maintenant, lancez 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 est d'exporter le composant App.

 exporter l'application par défaut; 
Les images dans la grille réactive. ( Grand aperçu )

La branche correspondante à ce stade est 01-make-api-calls .

Étendons maintenant cela en affichant plus d'images lorsque les pages défilent.

Implémentation d'Infinite Scroll

Notre objectif est de présenter plus d'images 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 avons seulement besoin d'augmenter la valeur de page . Nous devons également le faire lorsque nous n'avons plus d'images à afficher. Pour notre objectif 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 à atteindre cet objectif.

Ouvrez src / App.js et créez un nouveau réducteur, pageReducer ci-dessous imgReducer .

 // App.js
const imgReducer = (état, action) => {
  ...
}
const pageReducer = (état, action) => {
  commutateur (action.type) {
    cas 'ADVANCE_PAGE':
      return {... state, page: state.page + 1}
    défaut:
      état de retour;
  }
}
const [ pager, pagerDispatch ] = useReducer (pageReducer, {page: 0}) 

Nous définissons un seul type d'action. Chaque fois que l'action ADVANCE_PAGE est déclenchée, la valeur de la page est incrémentée de 1.

Mettez à jour l'URL dans la fonction fetch pour accepter dynamiquement les numéros de page. comme indiqué ci-dessous.

 fetch (`https://picsum.photos/v2/list?page=$ {pager.page} & limit = 10`) 

Ajoutez pager.page à la tableau de dépendances à côté 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 API, entrez le code ci-dessous. Mettez également à jour votre ligne d'importation.

 // App.js
import React, {useEffect, useReducer, useCallback, useRef} de 'react';
useEffect (() => {
  ...
}, [ imgDispatch, pager.page ])

// implémente le défilement infini avec l'observateur d'intersection
laissez bottomBoundaryRef = useRef (null);
const scrollObserver = useCallback (
  nœud => {
    nouveau IntersectionObserver (entrées => {
      entries.forEach (en => {
        if (en.intersectionRatio> 0) {
          pagerDispatch ({type: 'ADVANCE_PAGE'});
        }
      });
    }) .observer (nœud);
  },
  [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 des composants, c'est-à-dire que la valeur actuelle de la variable persiste lorsque le composant contenant est rendu à nouveau. La seule façon de modifier sa valeur consiste à réaffecter la propriété .current à cette variable.

Dans notre cas, bottomBoundaryRef.current commence par une valeur de null . Au fur et à mesure du déroulement du cycle de rendu de page, nous définissons sa propriété actuelle comme étant le nœud

.

Nous utilisons l'instruction d'affectation ref = {bottomBoundaryRef} pour indiquer à React de définir bottomBoundaryRef.current pour être le div où cette affectation est déclarée.

Ainsi,

 bottomBoundaryRef.current = null 

à la fin du cycle de rendu, devient:

 bottomBoundaryRef.current = 

Nous verrons où cette affectation est effectuée dans une minute.

Ensuite, nous définissons une fonction scrollObserver dans laquelle placer 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 en fait une dépendance est réexécuté. Cette réexécution, à son tour, appelle l'appel de récupération avec le nouveau numéro de page.

Le cortège d'événements ressemble à ceci.

Frappez l'intersection sous observation → appelez ADVANCE_PAGE action → incrémentez la valeur de pager.page par 1 → useEffect crochet pour les appels à récupérer → l'appel à chercher est exécuté → les images renvoyées sont concaténées au tableau images .

Nous appelons 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 dans un crochet useEffect la fonction s'exécuterait à chaque rendu de page.

Rappelons que bottomBoundaryRef.current fait référence à

. Nous vérifions que sa valeur n'est pas nulle avant de la passer à scrollObserver . Sinon, le constructeur IntersectionObserver retournerait une erreur.

Parce que nous avons utilisé scrollObserver dans un crochet useEffect nous devons l'envelopper dans un useCallback hook pour empêcher la restitution des composants sans fin. Vous pouvez en savoir plus sur useCallback dans la documentation React.

Saisissez le code ci-dessous après la division

 // App.js
...
{imgData.fetching && (   

Obtention d'images

)}

Lorsque l'appel API démarre, nous définissons la récupération sur true et le texte Obtention d'images devient visible. Dès qu'il est terminé, nous définissons aller chercher sur faux et le texte est masqué. Nous pourrions également déclencher l'appel d'API avant d'atteindre la limite exactement en définissant un seuil 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 stade est 02-infinite-scroll .

Nous allons maintenant implémenter le chargement paresseux de l'image.

Implémentation du chargement paresseux des images

Si vous inspectez l'onglet réseau pendant que vous faites défiler vers le bas, vous verrez que dès que vous frappez la ligne rouge (la limite inférieure), l'appel d'API se produit et toutes les images commencent chargement même lorsque vous n'avez pas pu les visualiser. Il existe diverses raisons pour lesquelles ce comportement n'est peut-être pas souhaitable. Nous souhaitons peut-être enregistrer les appels réseau jusqu'à ce que l'utilisateur souhaite voir une image. Dans un tel cas, nous pourrions opter pour le chargement des images paresseusement, c'est-à-dire que nous ne chargerons pas une image avant qu'elle ne défile dans la vue.

Ouvrez src / App.js . Juste en dessous des fonctions de défilement infinies, entrez le code suivant.

 // App.js

// paresseux charge les images avec l'observateur d'intersection
// n'échange la source de l'image que si la nouvelle URL existe
const imagesRef = useRef (null);
const imgObserver = useCallback (node ​​=> {
  const intObs = new IntersectionObserver (entrées => {
    entries.forEach (en => {
      if (en.intersectionRatio> 0) {
        const currentImg = en.target;
        const newImgSrc = currentImg.dataset.src;
        // n'échange la source de l'image que si la nouvelle URL existe
        if (! newImgSrc) {
          console.error ('La source de l'image n'est pas valide');
        } autre {
          currentImg.src = newImgSrc;
        }
        intObs.unobserve (nœud); // détache l'observateur une fois terminé
      }
    });
  })
  intObs.observe (nœud);
}, []);
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 d'image sur l'élément. Notez que nous vérifions d'abord si la nouvelle source d'image existe avant d'effectuer le swap. Comme avec la fonction scrollObserver nous encapsulons imgObserver dans un crochet useCallback pour empêcher un nouveau rendu des composants.

Notez également que nous arrêtons l'observateur d'un img élément une fois la substitution terminée. Nous le faisons avec la méthode unobserve .

Dans le crochet useEffect suivant, nous saisissons toutes les images avec une classe de .card-img-top sur la page avec document.querySelectorAll . Ensuite, nous itérons sur chaque image et définissons un observateur dessus.

Notez que nous avons ajouté imgData.images comme dépendance du crochet useEffect . Lorsque cela change, il déclenche le crochet useEffect et à son tour imgObserver est appelé avec chaque élément .

Mettez à jour l'élément comme indiqué ci-dessous.

 {author} 

Nous définissons une source par défaut pour chaque élément et stockons l'image que nous voulons afficher dans 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 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 de phare par défaut toujours affichée dans certains des espaces

Les images sont chargées paresseusement. ( Grand aperçu )

La branche correspondante à ce stade est 03-lazy-loading .

Voyons maintenant comment nous pouvons résumer toutes ces fonctions afin qu'elles soient

Résumé de l'extraction, du défilement infini et du chargement paresseux dans des crochets personnalisés

Nous avons réussi à implémenter l'extraction, le défilement infini et le chargement paresseux des images. Nous pouvons avoir un autre composant dans notre application qui nécessite des fonctionnalités similaires. Dans ce cas, nous pourrions résumer 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 crochet personnalisé comme une fonction JavaScript dont le nom commence par "utiliser" et qui peut appeler d'autres crochets. Dans notre cas, nous voulons créer trois crochets, useFetch useInfiniteScroll useLazyLoading .

Créez un fichier à l'intérieur du src / dossier. Nommez-le customHooks.js et collez le code ci-dessous à l'intérieur.

 // customHooks.js

import {useEffect, useCallback, useRef} de 'react';
// effectuer des appels API et transmettre les données renvoyées via dispatch
export const useFetch = (données, répartition) => {
  useEffect (() => {
    dispatch ({type: 'FETCHING_IMAGES', extraction: true});
    récupérer (`https://picsum.photos/v2/list?page=$ {data.page} & limit = 10`)
      .then (data => data.json ())
      .then (images => {
        dépêche ({type: 'STACK_IMAGES', images});
        dispatch ({type: 'FETCHING_IMAGES', extraction: false});
      })
      .catch (e => {
        dispatch ({type: 'FETCHING_IMAGES', extraction: false});
        return e;
      })
  }, [dispatch, data.page])
}

// bloc de code suivant ici 

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 API au composant App tandis que l'objet de données nous permet de mettre à jour l'URL du point de terminaison API.

 // défilement infini avec observateur d'intersection
export const const useInfiniteScroll = (scrollRef, dispatch) => {
  const scrollObserver = useCallback (
    nœud => {
      nouveau IntersectionObserver (entrées => {
        entries.forEach (en => {
          if (en.intersectionRatio> 0) {
            expédition ({type: 'ADVANCE_PAGE'});
          }
        });
      }) .observer (nœud);
    },
    [dispatch]
  );
  useEffect (() => {
    if (scrollRef.current) {
      scrollObserver (scrollRef.current);
    }
  }, [scrollObserver, scrollRef]);
}

// bloc de code suivant ici 

Le crochet useInfiniteScroll accepte une fonction scrollRef et une fonction dispatch . Le scrollRef nous aide à configurer l'observateur, comme déjà expliqué 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.

 // images de chargement paresseux avec observateur d'intersection
export const useLazyLoading = (imgSelector, items) => {
  const imgObserver = useCallback (node ​​=> {
  const intObs = new IntersectionObserver (entrées => {
    entries.forEach (en => {
      if (en.intersectionRatio> 0) {
        const currentImg = en.target;
        const newImgSrc = currentImg.dataset.src;
        // n'échange la source de l'image que si la nouvelle URL existe
        if (! newImgSrc) {
          console.error ('La source de l'image n'est pas valide');
        } autre {
          currentImg.src = newImgSrc;
        }
        intObs.unobserve (nœud); // détache l'observateur une fois terminé
      }
    });
  })
  intObs.observe (nœud);
  }, []);
  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 rechercher les images. Tout changement dans le tableau déclenche le crochet 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 extrait dans un nouveau fichier. La bonne chose maintenant, c'est que nous pouvons passer des arguments de manière dynamique. Utilisons maintenant ces crochets 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 paresseux des images. Laissez les réducteurs et les sections où nous utilisons useReducer . Collez le code ci-dessous.

 // App.js

// importer des crochets personnalisés
import {useFetch, useInfiniteScroll, useLazyLoading} à partir de './customHooks'

  const imgReducer = (état, action) => {...} // conserver cette
  const pageReducer = (état, action) => {...} // conserver ceci
  const [pager, pagerDispatch] = useReducer (pageReducer, {page: 0}) // conserve cette
  const [imgData, imgDispatch] = useReducer (imgReducer, {images: []extraction: true}) // conserve cette

laissez bottomBoundaryRef = useRef (null);
useFetch (pager, imgDispatch);
useLazyLoading ('. card-img-top', imgData.images)
useInfiniteScroll (bottomBoundaryRef, pagerDispatch);

// conserve le bloc de retour
revenir (
  ...
) 

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. Ce faisant, nous n'avons pas besoin de le spécifier document.querySelectorAll . useInfiniteScroll accepte à la fois une fonction ref et la fonction dispatch pour incrémenter la valeur de page .

La branche correspondante à ce stade est 04-custom-hooks .

Conclusion

Le HTML s'améliore en fournissant de belles API pour implémenter des fonctionnalités intéressantes. Dans ce post, nous avons vu à quel point il est facile d'utiliser l'observateur d'intersection dans un composant fonctionnel React. Dans le processus, nous avons appris à utiliser certains des crochets de React et à écrire nos propres crochets.

Ressources