# React Hooks Personnalisés : useInView

Précédemment, nous avons implémenté le Hook [useAudio](https://blog.iamludal.fr/react-hooks-personnalises-useaudio)
pour simplifier la gestion des fichiers audio. Aujourd'hui, on implémente un Hook qui suit la visibilité
de nos composants à l'écran : `useInView`.

## Motivation

Tout d'abord, penchons-nous sur un cas concret afin de motiver l'implémentation de notre Hook. L'exemple le plus évident est celui des scroll infinis, comme votre fil _Facebook_ ou _Instagram_. Comme vous le savez peut-être, ces applications ne chargent pas votre fil dans son entièreté (si c'était le cas, l'arbre DOM contiendrait des centaines, voire des milliers d'éléments, ce qui dégraderait considérablement leurs performances). À la place, elles chargent un certain nombre de publications, disons une dizaine et attendent que vous scrolliez un peu pour en charger une dizaine de plus. Lorsque vous continuerez de scroller, elles chargeront une dizaine de publications supplémentaires, et ainsi de suite. De ce fait, en faisant défiler votre fil, vous avez l'impression d'être face à une liste infinie.

Pour parvenir à ce résultat, ces applications utilisent les [Intersection Observers](https://developer.mozilla.org/fr/docs/Web/API/Intersection_Observer_API), qui attendent qu'un certain élément choisi soit au-dessus d'un certain seuil (appelé _root margin_) pour déclencher le chargement de nouveaux posts, comme le montre la figure ci-dessous.

![Schéma Intersection Observers](https://i.imgur.com/XHvm8bS.png)

Par exemple, lorsque le 4ᵉ élément sera visible à plus de 25% au-delà ce cette _root margin_ (ce seuil peut être configuré), une fonction callback sera appelée pour afficher de nouveaux posts. Ce comportement est celui que notre Hook devra implémenter.

Si les _Intersection Observers_ vous semblent flous, pas d'inquiétude : j'ai créé une petite démonstration avec laquelle vous pouvez jouer pour mieux comprendre leur fonctionnement. Si cela vous intéresse, vous pouvez y accéder via ce lien [CodeSandbox](https://codesandbox.io/s/intersection-observers-demo-16u0eg?file=/src/App.js).

Nous pouvons maintenant passer aux choses sérieuses et implémenter ce Hook. 👨🏻‍💻

## Implémentation

Avant toute chose, parlons de sa signature : comment souhaitons-nous l'utiliser ? Premièrement, nous voulons qu'il nous retourne une valeur booléenne nous indiquant si l'élément cible est visible ou non.

```jsx
const isIntersecting = useInView();
```

Mais nous avons un premier problème : comme nous l'avons vu dans le schéma de la section précédente, la _root margin_ n'est pas directement en bas de l'écran. À la place, elle peut être plus bas (ou plus haut), et nous pouvons définir nous-même ce décalage (par exemple, `100px`). Nous pouvons également définir l'élément _root_, au sein duquel l'élément cible pourra être visible ou non, ainsi que le seuil de visibilité pour déclencher la fonction de callback (`0%`, `50%`, `100%`, ...). Dans notre exemple de scroll infini, l'élément `root` correspond à la zone d'affichage du navigateur (_viewport_), la _root margin_ pourrait être définie à `100px`, le seuil d'affichage pourrait être à `0`, et l'élément cible pourrait être le pied de page (_footer_). De ce fait, dès que le _footer_ arrivera à `100px` du bas de l'écran, la fonction de chargement de nouveaux posts sera déclenchée.

Sur la documentation officielle des _Intersection Observers_, on peut voir qu'un objet `options` peut être passé en argument lors de l'instanciation. Nous utiliserons ce même objet afin de garder la même logique plutôt que créer la nôtre, ce qui pourrait être perturbant pour nos collègues par exemple. Ainsi, la signature de notre Hook sera modifiée comme suit :

```jsx
const options = { root: someElement, rootMargin: '100px', threshold: 0.25 };
const isIntersecting = useInView(options);
```

> Chaque clé de l'objet `options` est optionnelle.

Voilà qui est mieux. Nous avons cependant un dernier souci : à quel moment définissons-nous l'élément cible ? De plus, nous ne voulons pas nous embêter avec les méthodes natives du DOM, telles que `document.querySelector` ou `document.findElementById`. Au lieu de ça, nous allons utiliser les _refs_ (références) React pour nous simplifier la tâche et être plus dans la logique de composants. Notre Hook devra donc recevoir un autre paramètre : la _ref_ de l'élément cible.

```jsx
const target = useRef(null)
const options = { ... }
const isIntersecting = useInView(target, options)

// ...

return <p ref={target}>Loading...</p>
```

>  À noter que le paramètre `options` est totalement optionnel et pourra donc être omis.

Parfait, nous sommes désormais prêts à nous attaquer concrètement à l'implémentation. Commençons par définir le squelette du Hook.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  return isIntersecting;
};
```

Ensuite, nous allons créer la logique des _Intersection Observers_. Pour ce faire, on va instancier un `IntersectionObserver` dès que le Hook sera monté. Comme nous l'avons vu précédemment, le constructeur prend en paramètre la fonction callback qui sera exécutée lorsque l'élément cible atteindra le seuil défini. Cette fonction recevra en argument la liste des entrées concernées, chacune correspondant à un seuil franchi pour un des éléments observés (dans notre cas, nous n'aurons qu'un seul élément cible et cette liste contiendra donc un seul élément). La fonction callback est donc aussi simple que ça :

```jsx
const callback = entries => {
  setIsIntersecting(entries[0].isIntersecting);
};
```

Génial. Avec cette fonction callback et les options que nous recevons en arguments, nous pouvons instancier un `IntersectionObserver`.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const callback = entries => {
      setIsIntersecting(entries[0].isIntersecting);
    };

    const _observer = new IntersectionObserver(callback, options);
    setObserver(_observer);
  }, []);

  return isIntersecting;
};
```

Pour l'instant, rien ne se passe. En effet, nous devons observer l'élément cible après avoir créé cette instance, afin que la fonction callback soit appelée en conséquence lorsque cela sera nécessaire.

```jsx
useEffect(() => {
  const callback = entries => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  const _observer = new IntersectionObserver(callback, options);
  _observer.observe(target);
  setObserver(_observer);
}, []);
```

Super. Maintenant, nous devons faire attention à quelque chose : lorsque le Hook sera démonté, nous devrons arrêter d'observer l'élément cible afin que la fonction callback ne soit pas appelée lorsqu'elle ne devrait pas. Pour ce faire, nous allons renvoyer une fonction de nettoyage dans l'appel à `useEffect`, qui se chargera de se déconnecter de l'_observer_ quand le Hook sera démonté.

```jsx
useEffect(() => {
  const callback = entries => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  const _observer = new IntersectionObserver(callback, options);
  _observer.observe(target);
  setObserver(_observer);

  return () => {
    observer?.disconnect();
  };
}, []);
```

>  On utilise ici l'opérateur de chaînage optionnel (`?.`) pour éviter de rencontrer des erreurs si,
>  pour une raison ou une autre, l'*observer* est `null`.

Voilà qui est beaucoup mieux ! Nous nous rapprochons de la fin. Il ne nous reste qu'une chose à régler : nous devons écouter les changements de valeur de nos arguments. Par exemple, si l'élément cible ou le seuil de visibilité changent, nous devons mettre à jour l'_observer_ en conséquence. Pour ce faire, nous allons les ajouter à la liste de dépendences du Hook `useEffect`, dans lequel nous nous déconnecterons du précédent _observer_ (s'il existe) pour en créer une nouvelle instance avec les valeurs mises à jour.

```jsx
useEffect(() => {
  const callback = entries => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  observer?.disconnect(); // On se déconnecte du précédent observer

  // target.current peut être null, auquel cas on ne fait rien
  if (target.current) {
    const _observer = new IntersectionObserver(callback, options);
    _observer.observe(target);
    setObserver(_observer);
  }

  return () => {
    observer?.disconnect();
  };
}, [target.current, options.root, options.rootMargin, options.threshold]);
```

Eh voilà, notre Hook est maintenant terminé et prêt à être utilisé. Voici le résultat final.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const callback = entries => {
      setIsIntersecting(entries[0].isIntersecting);
    };

    observer?.disconnect();

    if (target.current) {
      const _observer = new IntersectionObserver(callback, options);
      _observer.observe(target);
      setObserver(_observer);
    }

    return () => {
      observer?.disconnect();
    };
  }, [target.current, options.root, options.rootMargin, options.threshold]);

  return isIntersecting;
};
```

## Utilisation

Dans la première partie de cet article, nous avons parlé de _Facebook_ pour introduire les _Intersection Observers_. Nous allons simuler un concurrent, que l'on appellera _Fuzebook_. Dans l'extrait de code ci-dessous, nous chargeons le fil de l'utilisateur. L'élément cible de notre _Intersection Observer_ sera un paragraphe situé en bas de la page, qui contient le texte "Loading more posts...". On pourrait également utiliser une autre cible, comme un _spinner_ ou le pied de page. Dans tous les cas, cet élément ne devrait pas apparaître à l'écran, car nous définissons une _root margin_ de `150px`. Il pourrait éventuellement être visible si l'appel à `fetch` (de la fonction `getPosts`) prend quelques secondes, par exemple en cas de mauvaise connexion internet.

Dans notre exemple, le scroll est infini : nous n'atteindrons jamais le bas de la page étant donné que le fil contient des centaines, voire des milliers de publications. Il serait donc inutile d'avoir un pied de page par exemple, car on ne le verrait jamais. Si la section de scroll de votre application contient moins d'éléments (moins d'une centaine), vous devriez gérer le cas où il n'y plus d'éléments à charger (il suffirait de masquer l'élément cible, c'est-à-dire le paragraphe en bas de la page ou le spinner).

Voici le code du composant principal de notre application _Fuzebook_.

```jsx
const App = () => {
  const posts = useArray([]);
  const page = useCounter(1);
  const loadingElement = useRef(null);
  const isIntersecting = useInView(loadingElement, {
    threshold: 1,
    rootMargin: '150px',
  });

  // Chargement de nouveaux posts
  useEffect(() => {
    getPosts(page.value).then(newPosts => {
      posts.concat(newPosts);
    });
  }, [page.value]);

  useEffect(() => {
    if (isIntersecting) {
      page.increment();
    }
  }, [isIntersecting]);

  return (
    <div className="App">
      <h1>Fuzebook</h1>
      <ul>
        {posts.value.map((post, i) => (
          <Card key={i} post={post} />
        ))}
      </ul>
      <p ref={loadingElement}>Loading more posts...</p>
    </div>
  );
};
```

Comme vous pouvez le voir, nous avons réutilisé 2 Hooks personnalisés que nous avons implémenté dans
des articles précédents : [useArray](https://blog.iamludal.fr/react-hooks-personnalises-usearray) et
[useCounter](https://blog.iamludal.fr/react-hooks-personnalises-usecounter). Cela simplifie le code
du composant `App`, aboutissant à un code bien plus propre et plus lisible. Le composant `Card`
affiche une simple publication et la fonction `getPosts` (qui renvoie une promesse) récupère des
publications depuis une API quelconque.

## Conclusion

Cet article clôture la série sur les Hooks personnalisés en React, durant laquelle nous avons découvert comment extraire de la logique au sein de fonctions réutilisables afin de simplifier notre code. Cela nous permet également de réutiliser de la logique commune à travers notre application sans avoir à dupliquer de code. De ce fait, nous suivons le Principe de Responsabilité Unique (ou SRP : _Simple Responsability Principle_), à la fois pour nos composants (qui se concentrent désormais uniquement sur leur responsabilité) et pour nos Hooks. J'espère que vous avez pris autant de plaisir à suivre cette série que j'en ai eu à l'écrire, et je vous donne rendez-vous dans de prochains articles. 👋

---

Code source disponible sur [CodeSandbox](https://codesandbox.io/s/custom-react-hooks-useinview-6cev5y?file=/src/App.tsx).

