Hooks.png

React Hooks

par Paul · · pas de commentaire

Hooks

Les Hooks de React utilisent des caractéristiques de l'environnement React en appelant des fonctions spécifiques,les Hooks, depuis un composant.

Dans React, on peut construire des composants statiques ou jetables en écrivant des fonctions.

Cependant les composants d'interface sont souvent dynamiques; ils peuvent avoir besoin de :

  • changer l'état de leurs données
  • réagir aux événements de cycle de vie React
  • accéder aux éléments du DOM
  • bien d'autres choses.

Mais avant React 16.8, les développeurs devaient écrire des classes pour pouvoir profiter de certains avantages de React. Dorénavant on peut, en plus, profiter des Hooks.

D'une façon général les Hooks permettent de construire de façon plus ergonomique les composants car on peut réutiliser le state (l'état des données d'un composant) pour s'en resservir sans changer la hiérarchie de vos composants.

  • Avant les Hooks
class Btn extends React.Component {
  construstor(props) {
    super(props);
    this.state = { nbClick: 0 };
  }
  render() {
    return <p>{this.state.nbClick}</p>;
  }
}
  • Avec les Hooks
function newBtn() {}
 const [state] = useState({ nbClick: 0 });
 return <p>{state.nbClick}</p>
}

React a 10 Hooks prédéfinis et j'espère en faire le tour dans cet article. On pourra ensuite en construire un entièrement en extrayant la logique d'un de vos composant en une fonction réutilisable.

Contexte

Si vous n'êtes pas très familier avec React, il est important de comprendre pourquoi les Hooks existent. Par le passé, la logique de filtrage par l'état (une donnée qui change à l'intérieur de l'application) était étroitement liée à un composant définit avec une classe.

class Btn extends React.Component {
>  construstor(props) {
>    super(props);
>    this.state = { nbClick: 0 };
>  }
  render() {
    return <p>{this.state.nbClick}</p>;
  }
}
```

Cela veut dire que pour fonctionner avec des données dynamiques, on devait créer de nouveau composant. Ça semble raisonnable, mais cela mène en réalité à un arbre complexe, composé de multiples nœud imbriqués.

React était mal aimé pour cela. Partager du code entraînait de la frustration, comme les composant de haut ordre et les propriétés de rendu qui sont des modèles qui obligent à passer en tant qu'arguments des composants à d'autres composants. Cela peut sembler plus complexe que nécessaire par rapport à d'autres modèles de certain environnement comme AngularJS.

function composantDeHautNiveau(ComposantWrapped) {
  return class extends React.Component {
    render() {
      return <ComposantWrapped {...this.props} />;
    }
  };
}

Par chance les Hooks changent tout, en donnant l'accès à des caractéristiques de bas niveau de React en dehors du contexte du composant. Quand on utilise les Hooks, on peut les voir comme des blocs de construction de l'environnement React qui donnent des possibilités inaccessibles avec du pur JavaScript.

Les Hooks sont des fonctions qui commencent toujours avec le terme use car vous utilisez la puissance de l'environnement React.

Avant de commencer à utiliser les Hooks, il y a une règle qui est de toujours les utiliser au plus haut niveau d'un composant fonctionnel.

function App() {
  useHook(); // Valide

  // Nope !
  const f = () => {
    useHook();
  };
  // Nope !
  return <button onClick={() => useHook()} />;
}

Ils ne fonctionnent pas dans une fonction JavaScript normale, dans des fonctions imbriqués, les boucles et toutes choses similaire. La seule exception est lors de la création de Hook personnalisé, nous verrons ça à la fin de l'article.

Penchons nous à présent sur ce que font vraiment ces différents Hooks.

useState()

C'est facilement le plus utilisé et important des Hooks. On l'importe de React pour l'utiliser

import { useState } from "react";

La mission de useState() est de gérer les données dynamiques. Toutes les données qui changent dans l'application sont appelées state. Lorsque le state change, on veut que React met à jour l'interface pour que les dernières modifications soit visibles pour l'utilisateur final.

Un Hook prend un argument optionnel, il s'agira alors du state par défaut.

function App(){
  useState(0)

  ...
}

La fonction renvoyée est un tableau qui contient deux valeurs utilisable à l'intérieur du composant. Il s'agit d'un tableau car JavaScript permet de le déstructurer afin de simplement assigner les valeurs à des variables locales nommées comme bon vous semble.

function App(){
  const [compteur, setCompteur] = useState(0)
  //       [state, setter]
  ...
}

La première valeur est le state, si on le référence par la suite dans l'interface et que sa valeur évolue par la suite, React va automatiquement reconstruire l'interface pour afficher la dernière valeur.

function App() {
  const [compteur, setCompteur] = useState(0);
  return <button>{compteur}</button>;
}

Le deuxième argument est une fonction de mise à jour du state.

function App() {
  const [compteur, setCompteur] = useState(0);
  return <button onClick={() => setCompteur(compteur + 1)}>{compteur}</button>;
}

useEffect()

Le deuxième Hook le plus important, mais souvent un des plus confus. Pour mieux le comprendre, il faut connaître ce qu'est le cycle de vie d'un composant. Dans un composant définit par une classe, on peut implémenter les méthodes suivantes qui gèrent les événements du cycle de vie.

componentDidMount(){
 // initialisé
}
componentDidUpdate(){
 // mise à jour du state
}
componentWillUnmount(){
 // détruit
}

Le premier événement se déclenche une fois lorsque le composant est ajouté à l'interface (ou monté). Par la suite les données dynamiques du composant peuvent changer ou bien il est mis à jour, ce qui peut arriver plusieurs fois. Enfin, à un moment donné le composant va être supprimé de l'interface (démonté).

Maintenant que l'on a une représentation de ces trois événements du cycle de vie d'un composant, on va pouvoir se pencher sur le Hook useEffect().

useEffect() permet décrire la logique de tous ces événement dans une seule fonction API. C'est une fonction qui prend comme premier argument une fonction que vous définissez. React va exécuter cette fonction (ou ses effets secondaires) après avoir mis à jour le DOM.

import { useEffect, useState } from "react";

function Bouton() {
  const [compteur, setCompteur] = useState(0);

  useEffect(() => {
    alert("Salut !");
  });
  ...
}

Dans sa version actuelle, il exécutera la fonction à chaque fois que le state du composant change. Cela veut dire qu'elle sera exécutée une fois avec sa valeur par défaut lors de l'initialisation du composant puis chaque fois que le state sera mis à jour. Dans la plupart des cas, on voudra un contrôle plus fin de ce comportement. Si on veut, par exemple, allez cherchez des données lors de l'initialisation puis mettre à jour le state de façon asynchrone lorsqu'elles sont entièrement reçues.

import { useEffect, useState } from "react";

function Bouton() {
  const [compteur, setCompteur] = useState(0);
  const [recues, setRecues] = useState(false);

  useEffect(() => {
    fetch('foo').then(() => setRecues(true))
  });
  ...
}

Ce code provoquerait une boucle infinie puisque chaque mise à jour provoquerai une nouvelle demande des données qui ensuite provoquerai une mise à jour, etc.

Pour remédier à cette situation on passe un second argument à useEffect() sous la forme d'un tableau de dépendances. Si le tableau est vide cela signifie que le React n'exécutera qu'une seule fois la fonction lors de l'initialisation du composant.

import { useEffect, useState } from "react";

function Bouton() {
  const [compteur, setCompteur] = useState(0);
  const [recues, setRecues] = useState(false);

  useEffect(
    () => {
      fetch('foo').then(() => setRecues(true))
    },
    [] // éxécution unique à l'initialisation
  );
  ...
}

Sinon on voudra ré-exécuter celle-ci chaque fois qu'une donnée change. Il suffit dans ce cas de l'ajouter dans le tableau des dépendances.

import { useEffect, useState } from "react";

function Bouton() {
  const [compteur, setCompteur] = useState(0);
  const [recues, setRecues] = useState(false);

  useEffect(
    () => {
      fetch('foo').then(() => setRecues(true))
    },
    [compteur] // éxécution lorsque la valeur des dépendances change
  );
  ...
}

Désormais React va observer la valeur de cette donnée et à chaque changement exécutera la fonction.

Un dernier cas que l'on aimerait traiter avec useEffect() serait l'exécution d'une dernière fonction lors de la destruction du composant. La procédure pour implémenter une telle fonction est de la retourner avec le mot clef return dans useEffect().

import { useEffect, useState } from "react";

function Bouton() {
  const [compteur, setCompteur] = useState(0);

  useEffect(() => {
    alert("Salut !");

    return () => alert("Adieu composant.")
  });
  ...
}

React va l'interpréter lors de la destruction du composant.

useContext()

Ce Hook permet d'utiliser le contexte de l'API React, c'est un mécanisme qui autorise le partage ou définit la porté de valeurs à travers l'entièreté de l'arbre des composants.

Imaginons un objet appelé humeurs, qui peut être joyeux ou triste. Pour partager l'humeur actuel parmi différents composants déconnectés, on peut créer un contexte. Une part de l'application peut être joyeuse, nous utilisons donc un contextProvider pour définir l'environnement où l'humeur sera joyeux.

const humeurs = {
  joyeux: "=D",
  triste: "T_T",
};
const HumeursContexte = createContext(humeurs);

function App(props) {
  return (
    <HumeursContexte.Provider value={humeurs.joyeux}>
      <HumeurAffichee />
    </HumeursContexte.Provider>
  );
}

Maintenant n'importe quel composant enfant héritera de cette valeur sans nécessiter de passer des propriétés à tous les descendants. Cela mène enfin au Hook useContext(), il permet d'accéder à la valeur du fournisseur de contexte, qui peut être bien des niveaux au dessus dans l'arbre des composants. Accéder à une valeur avec useContext() est facile et évite de passer des propriétés tout au long de l'arbre des composants.

function HumeurAffichee() {
  const humeur = useContext(HumeurContexte);

  return <p>{humeur}</p>;
}

Ainsi si l'humeur change de joyeux à triste dans le fournisseur de contexte, la valeur sera mise à jour automatiquement. Si vous avez déjà utilisé React, vous avez sans doute déjà utilisé le composant Consumer, useContext() est essentiellement un remplaçant plus propre de celui-ci.

useRef()

Ce Hook, vous aide à créer un objet transformable qui gardera la même référence entre chaque rendu du composant. Il peut être utile lorsque vous avez une valeur qui change, un peu à la façon de useState(), mais la différence est que cela ne déclenche pas de nouveau rendu lors du changement de la valeur. Par exemple, si on créer un bouton qui compte le nombre de clic avec useRef(), on pourrait lier le compte en appelant compteur.current.

function App() {
  const compteur = useRef(0);

  return <button onClick={() => compteur.current++}>{compteur.current}</button>;
}

Cependant lors d'un clic sur le bouton, le compteur ne changera jamais dans l'interface, car useRef() ne génère pas de nouveau rendu comme useState() le fait. Cela peut être utile lorsqu'on manipule un objet transformable.

Mais le cas le plus commun avec useRef() est de lier un élément HTML depuis le DOM. On peut commencer par créer une référence nulle appelée monCompteur puis le connecter au bon élément HTML de type button en utilisant l'attribut ref. Ensuite, il est possible de lier l'élément HTML dans une fonction pour appeler une interface native du DOM, telle que click() dans cet exemple, ce qui va générer un clic du bouton.

function App() {
  const monCompteur = useRef(null);

  const clic = () => monCompteur.current.click();
  return <button ref={monCompteur}></button>;
}

En bref, si vous avez besoin d'attraper un élément du DOM, useRef() est le Hook qui s'y prête.

useReducer()

Ce qu'il fait est similaire à setState(), il le fait différemment, utilisant le modèle de REDUX: Au lieu de mettre à jour le state directement, on groupe les actions qui sont envoyées dans une fonction reducer et cette fonction détermine comment gérer l'étape suivante. Tout comme useState(), useReducer() renvoie un tableau de deux valeurs, la première la valeur est le state qu'on souhaite voir dans l'interface, la deuxième diffère ce n'est plus une fonction qui met à jour le state. Au lieu de cela on donne une fonction qui peut envoyer une action; une action est un simple objet qui à un type définit par n'importe qu'elle chaîne de caractères et un payload indéfini qui est optionnel.

On pourrait envoyer une action quand un bouton est cliqué, qui déclenchera notre fonction reducer. Notre fonction est définie et passée en argument au Hook useReducer(). Elle prend le state actuel et l'action comme arguments puis les utilise pour donner le nouveau state, souvent par l'utilisation d'un switch.

function reducer(state, action) {
  switch (action.type) {
    case "ajouter":
      return state + 1;
    case "soustraire":
      return state - 1;
    default:
      throw new Error();
  }
}
function App() {
  const [state, dispatch] = useReducer(reducer, 0);

  return (
    <>
      Compte = {state}
      <button onClick={() => dispatch({ type: "ajouter" })}>+</button>
      <button onClick={() => dispatch({ type: "soustraire" })}>-</button>;
    </>
  );
}

Dans cet exemple si l'action est ajouter on ajoute un, si c'est soustraire on soustrait un. Enfin le Hook useReducer() prend le state initial en second argument.

Il est légitime de se demander pourquoi voudrait-on utiliser cette syntaxe, car elle semble bien plus complexe que utiliser setState(). La réponse (controversée) est que tout monde ne croit pas que ça aide à maintenir le code alors que la complexité augmente. Au fur et à mesure que l'on ajoute des composants, il devient plus compliqué de gérer le state de façon sure et claire. Le modèle REDUX peut aider.

useMemo()

Il peut vous aider à optimiser les coûts de taches gourmandes ou améliorer le performances. Mais gardez en tête qu'on ne veut pas optimiser les performances prématurément, il faut penser à ce Hook comme un outils, une option pour gérer les plus grosses opérations qui vous savez vont impacter vos performances. Imaginons avoir encore un compteur, mais ensuite on calcule une propriété additionnelle appelée calculGourmant.

function App() {
  const [compte, setCompte] = useState(230);

  const calculGourmant = useMemo( () => {
    return compte ** 2;
  },[compte])

  ...
}

Plutôt que de recalculer à chaque rendu on peut mémoriser la valeur, on écrit une fonction qui renvoie la valeur calculée puis comme second argument on ajoute les dépendances pour déterminer quand le calcul doit être effectué. Dans ce cas, chaque fois que le compteur change.

useCallback()

C'est très bien de mémoriser des valeurs mais dans d'autres cas on voudra mémoriser une fonction entière. Quand on définit une fonction dans un composant, un nouvel objet fonction est créé chaque fois qu'un composant est re-rendu. Habituellement ce n'est pas important pour les performances mais dans certains cas on veut mémoriser une fonction. Un cas commun est quand vous passez la même fonction vers de multiples composants enfants. Avec des grandes listes par exemple.

function App() {
  const [compte, setCompte] = useState(230);

  const afficheCompte = useCallback(() => {
    alert(`Compte ${compte}`);
  }, [compte]);

  return (
    <>
      {" "}
      <ComposantEnfant handler={afficheCompte} />{" "}
    </>
  );
}

En entourant la fonction avec useCallback(), on peut empêcher des rendus non-désirés des enfants, car ils utilisent le même objet fonction.

useImperativeHandle()

Maintenant un des Hooks les plus confus : useImperativeHandle(). Si vous utilisez une bibliothèque de composants réutilisables dans React, vous pourriez avoir besoin d'accéder aux éléments DOM sous-jacents et ensuite le renvoyer pour être accessible par les Consumers de la bibliothèque. On peut accéder à un élément du DOM avec le Hook useRef() que l'on a montré plus haut. Ensuite on peut entourer le composant avec forwardRef() pour rendre cette référence disponible lorsque quelqu'un accède à ce composant.

function BoutonCool(props, ref) {
  const monBouton = useRef(null);

  return <button ref={monBouton}> </button>;
}

BoutonCool = forwardRef(BoutonCool);

useImperativeHandle() intervient si vous voulez changer le comportement d'une référence exposée. Vous pourriez vouloir modifier les méthodes de l'élément natif, mais concrètement le besoin de ce Hook est probablement assez rare.

function App() {
  const ref = useRef(null);
  return <BoutonCool ref={ref}></BoutonCool>;
}

function BoutonCool(props, ref) {
  const monBouton = useRef(null);

  useImperativeHandle(ref, () => ({
    click: () => {
      console.log("bouton clickable !");
      monBouton.current.click();
    },
  }));

  return <button ref={monBouton}> </button>;
}
BoutonCool = forwardRef(BoutonCool);

useLayoutEffect()

Un nouveau Hook rarement utile. Il fonctionne comme useEffect(), avec une petite différence, la fonction de retour est exécutée après le rendu du composant mais avant que les mises à jour soit affichées sur l'écran. Ce qui veux dire que React attendra que l'exécution se finisse avant de mettre à jour l'affichage pour l'utilisateur final. Mais il peut y avoir une situation où on a besoin de calculer une position de défilement ou quelque chose d'autre relatif à l'interface, avant que le DOM soit visuellement mis à jour.

function App() {
  const monBouton = useRef(null);

  useLayoutEffect(() => {
    const rect = monBouton.current.getBoundingClientRect();
    console.log(box.height);
  });

  return (
    <>
      <button ref={ref}></button>
    </>
  );
}

useDebugValue()

C'est le dernier Hook natif, mais il ne fera pas de sens tant qu'on ne construit pas ses propres Hooks personnalisés. Si vous affichez votre application dans votre navigateur et ouvrez l'outil de développement React vous verrez que chaque composant dans l'arbre vous donne un aperçu des Hooks définit ici.

Hooks in React Developer Tools

Le but de useDebugValue() est de rendre possible de définir vos propre labels personnalisés dans l'outil de développement lorsque vous construisez vos Hooks.

Pour voir cela en action, construisons notre Hook.

function App() {
  const [displayName, setDisplayName] = useState();

  useEffect(() => {
    const data = fetchFromDatabase(props.userId);
    setDisplayName(data.displayName);
  }, []);

  return <button>{displayName}</button>;
}

Remarquons comment notre composant utilise 2 Hooks ensembles, nous avons useState() pour définir un nom à afficher pour l'utilisateur et ensuite useEffect() pour récupérer la donnée depuis une base de donnée quelconque. Imaginons que nous avons dix composants qui ont besoin d'implémenter cette même logique. Avec des Hooks, on peut très facilement créer notre propre fonction appelée useDisplayName(). Il s'agit d'une simple fonction JavaScript qui implémente le même code qu'il y avait dans le composant.

function useDisplayName() {
  const [displayName, setDisplayName] = useState();

  useEffect(() => {
    const data = fetchFromDatabase(props.userId);
    setDisplayName(data.displayName);
  }, []);

  return displayName;
}
function App() {
  const displayName = useDisplayName();

  return <button>{displayName}</button>;
}

La seule différence est qu'on veut retourner le nom d'affichage depuis la fonction afin de pouvoir l'utiliser ailleurs dans l'application. On peut maintenant l'utiliser dans de multiple composants. Une dernière touche ici est d'ajouter useDebugValue() à notre Hook personnalisé. L'argument passé au Hook sera la valeur affichée dans l'outil de développement React.

function useDisplayName() {
  const [displayName, setDisplayName] = useState();

  useEffect(() => {
    const data = fetchFromDatabase(props.userId);
    setDisplayName(data.displayName);
  }, []);

  useDebugValue(displayName ?? "chargement...");

  return displayName;
}
function App() {
  const displayName = useDisplayName();

  return <button>{displayName}</button>;
}

Avec ce dernier Hook, nous en avons fait entièrement le tour.