React Hooks #1 : La base avec useState, useEffect et useRef

Je pensais que les Hooks de React étaient entrées dans les mœurs et que tout le monde était devenu expert. Cependant, après quelques discutions par-ci, par-là, j’ai découvert que ce n’était pas le cas. J’espère que cette suite d’article pourra les aiguiller vers la maitrise de cette “hype” du passé.

On commence comme toujours par la base. Pour moi, les Hooks les plus souvent utilisés sont les suivants :

  • useState
  • useEffect
  • useRef

On va découvrir et redécouvrir les Hooks ensemble. J’espère donc que vous possédez une connaissance minimale en JS et React.

Récap

Les Hooks sont une nouvelle forme de gestion du cycle de vie de React. Ils sont arrivés avec la version 16.8.0. Précédemment, les composants React pouvaient être écrit de 2 manières.

En utilisant une classe :

class Welcome extends React.Component {
  render() {
    return (
      <div>
        <p>Welcome</p>
      </div>
    );
  }
}

En utilisant une fonction :

function Welcome() {
  return (
    <div>
      <p>Welcome</p>
    </div>
  );
}

L’inconvénient des composants fonctionnelles était l’absence de du cycle de vie de React. Impossible d’utiliser du state ou d’interagir avec le composant en fonction de son état de rendu (componentDidMount, …).

Les Hooks arrivent donc ici. C’est ce qui va permettre de donner vie aux composants fonctionnels. Mais attention, React n’a pas mis ça en place uniquement afin de reproduire les effets du cycle de vie des classes. Ils ont décidé de changer le point de vue du développement pour avoir une approche, comme ces composants, plus fonctionnelle.

La base

L’utilisation des Hooks doit respecter deux règles fondamentales :

  • Les Hooks ne peuvent être appelés que dans un composant fonctionnel de React ou un autre Hook.
  • Les Hooks doivent toujours être appelés dans le même ordre. Dans votre composant, si les Hooks sont exécutés dans des conditions, boucles ou sous fonctions, l’ordre d’appel doit toujours être le même. Il est fortement recommandé de garder l’appel des Hooks au premier niveau du composant.

À partir de là vous pouvez appeler plusieurs fois le même dans un composant. Libre à vous de les utiliser dans l’ordre que vous souhaitez.

useState

C’est avec cette fonction qu’on va gérer l’état (comme son nom l’indique) du composant.

const [state, setState] = useState(initialState);

Voilà la syntaxe proposée par React. Qu’avons-nous là ?

  • [state, setState] : Le tableau retourné par la fonction useState contient la valeur de l’état (state) et la fonction permettant de l’éditer (setState).
  • state : la valeur de l’état que l’on va pouvoir utiliser partout dans le composant
  • setState : la fonction qui va permettre de mettre à jour l’état (setState(newValue))
  • initialState : la valeur initiale de l’état. Si aucune valeur n’est fournie, l’état est initialisé à null.

Et vous pouvez en créer plusieurs dans le même composant.

function Exemple() {
  const [total, setTotal] = useState(0);
  const [value, setValue] = useState("defaultValue");
  //[...]
}

C’est d’ailleurs la force de cette notation. On va préférer segmenter les états plutôt que de d’en avoir qu’un seul contenant plusieurs valeurs. Cela devient plus facile de séparer la logique et de mettre à jour l’état du composant.

Un exemple !

On va prendre le bon vieil exemple du compteur.

// Importation du useState
import React, { useState } from "react";

function Exemple() {
  // On initialise le state ici, avec 0 comme valeur par défaut
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* On utilise la valeur du state ici pour l'afficher */}
      <p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>

      {/* On utilise la fonction de modification pour ajouter 1 à la valeur actuelle */}
      <button onClick={() => setCount(count + 1)}>
        C'est ici qu'il faut cliquer
      </button>
    </div>
  );
}

L’exo

QUOI! Un exercice?

Eh oui, je ne vais pas être le seul à bosser ici !

Je vous ai créé un petit CodePen pour mettre en place votre premier (…ou pas) useState.

Dans celui-ci, vous retrouverez un compteur. Cependant, le bouton pour réduire le compte ne fonctionne pas. À vous de mettre en place cette fabuleuse nouvelle fonctionnalité. Et on aimerait que la dernière action soit affichée dessous. Histoire de savoir lequel des boutons a été cliqué en dernier.

Je vais quand même vous donner un indice : il faut utiliser useState. ;)

L’Exercice –> useState - Exercice

Note : Les imports dans un projet JS sont souvent sous la forme import { useState } from 'react'. Étant donné l’environnement de CodePen, les imports dans certains exemples seront sous la forme const { useState } = React.

Bonne chance !

À noter

setState

La fonction de modification d’état peut aussi prendre une fonction en paramètre. Dans ce cas, le paramètre de la fonction sera l’ancienne valeur de l’état.

const [count, setCount] = useState();
//[...]
// Cette notation est équivalente à setCount(count + 1)
setCount((oldCount) => oldCount + 1);

initialState

Le paramètre utilisé par useState (initialState) peut aussi être une fonction.

const [count, setCount] = useState(() => {
  const initialCount = internalCountMethodThatMayBeExpensiveSomeHow(props);
  return initialCount;
});

Cette fonction ne sera exécutée qu’au premier rendu.

Rendu

Si la nouvelle valeur de l’état est la même que la valeur actuelle, aucun rendu ne sera fait et donc aucun effet ne sera exécuté.

Attention: La comparaison effectué par React est un Object.is.

Solution

Voilà une des solutions possibles pour l’exercice plus haut :

useEffect

Maintenant qu’on sait comment gérer l’état de notre composant, on va s’intéresser aux “effets de bord”. Ce sont les actions qui peuvent affecter d’autres composants et qui ne peuvent pas être réalisées pendant l’affichage. On va retrouver le chargement de données depuis une source externe, les abonnements à des évènements du navigateur ou encore la réaction à l’évolution d’autres composants. Et pour faire tout ça, on va utiliser useEffect.

On aura donc cette syntaxe.

useEffect(effectFunction, conditionalArray);

On a donc:

  • effectFunction: fonction d’effet exécutée
  • conditionalArray: optionnel, tableau de propriétés conditionnant l’exécution de l’effet

La fonction d’effet s’exécute, par défaut, à chaque rendu. A chaque fois, qu’un des state ou qu’une des props est modifié.

Si on reprend l’exemple précédent, on peut ajouter un effet à chaque modification du compteur.

// Importation du useEffect
import React, { useState, useEffect } from "react";

function Exemple() {
  const [count, setCount] = useState(0);

  // initialisation de l'effet
  useEffect(() => {
    document.title = `Compteur : ${count}`;
  });

  return (
    <div>
      <p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>
      <button onClick={() => setCount(count + 1)}>
        C'est ici qu'il faut cliquer
      </button>
    </div>
  );
}

L’effet va s’exécuter au premier rendu et affichera donc dans le titre Compteur : 0. Puis à chaque fois que le composant fera un rendu. Dans notre cas, à chaque fois que le compteur sera modifié.

Exécution conditionnelle

La condition va permettre d’effectuer un effet en fonction d’une propriété ou de l’état du composant. Notre effet n’a pas besoin d’être mis à jour à chaque modification du composant, mais uniquement si le compte change. On va donc se retrouver avec la syntaxe suivante :

useEffect(() => {
  document.title = `Compteur : ${count}`;
}, [count]);

Dans l’exemple précédent, si notre composant Exemple recevait des propriétés, l’effet se serait exécuté. Une fois la condition ajoutée, l’effet ne s’active que lorsque la valeur count est modifiée.

L’effet peut donc aussi s’exécuter à la suite d’une modification de propriétés.

function Exemple(props) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // Si la valeur `value` reçu des propriétés change, le compteur se mettra à jour
    setCount(props.value);
    // On conditionne l'exécution de cet effet sur l'évolution de la propriété `value`
  }, [props.value]);

  return (
    <div>
      <p>{count}</p>
      {/* Mais la valeur du compteur peut toujours être modifiée avec le bouton */}
      <button onClick={() => setCount(count + 1)}> Plus 1 </button>
    </div>
  );
}

Nettoyage

Il est possible d’exécuter un effet de ‘nettoyage’ en retournant une fonction dans notre useEffect.

useEffect(() => {
  document.title = `Compteur : ${count}`;
  return () => {
    document.title = "";
  };
}, [count]);

Les effets se dérouleront dans l’ordre suivant :

  1. Le composant s’affiche pour la première fois
  2. document.title passe à 'Compteur : 0'
  3. Clic sur le compteur
  4. document.title passe à ''
  5. document.title passe à 'Compteur : 1'
  6. Le composant est supprimé (pour une quelconque raison)
  7. document.title passe à ''

C’est surtout utile pour les souscriptions à des évènements :

useEffect(() => {
  window.addEventListener("mouseup", handleMouseUp);
  return () => {
    window.removeEventListener("mouseup", handleMouseUp);
  };
});

L’exo

Mettons tout cela en pratique. On reprend le composant GreatCounter.

La prochaine fonctionnalité sera d’émettre une alerte lorsque le compteur est égal à 10 ou à -10.

Je vous laisse le soin de trouver les “wordings” pour ces alertes. ;)

Exercice : useEffect - Exercice

À noter

Plusieurs effets

Tout comme pour useState, vous pouvez utiliser plusieurs useEffect dans le même composant.

useEffect(() => {
  if (cartItems.length) {
    console.log("Il y a des objets dans le panier !");
  }
}, [cartItems]);

useEffect(() => {
  document.title = loading ? "Chargement en cours" : "La page est chargée";
}, [loading]);

Un seul effet

Comme vu plus haut, sans tableau de condition, l’effet s’exécute à chaque rendu. Avec un tableau de propriétés ou d’état, l’effet s’exécute lors de la modification de ceux-ci.

Du coup, pour que l’effet ne s’exécute qu’une seul fois, vous pouvez passer un tableau vide.

async function fetchData() {
  const res = await fetch("https://yourapi.org/");
  res.json().then(doWithResult).catch(manageError);
}

useEffect(() => {
  fetchData();
}, []);

Asynchrone

La notation ci-dessus n’est pas anodine. La fonction passée en premier paramètre à useEffect doit retourner une fonction ou rien. Ceci n’inclut pas les Promise. On ne peut donc pas utiliser async/await sur cette fonction.

La syntaxe suivante est donc erronée :

useEffect() => {
  // Permet de contrôler l'état du 'fetch'
  const controller = new AbortController();

  // On exécute la fonction asynchrone à l'intérieur du useEffect
  (async () => {
    try {
      const res = await fetch("https://yourapi.org/", { signal: controller.signal });
      const json = await res.json();

      // Utiliser les informations reçu (par exemple, modifier l'état du composant)
      await doWithResult(json);
    } catch (err) {
      manageError(err);
    }
  })();

  return () => {
    // Si le composant est supprimé avant la fin de la récupération de données, l'appel est annulé
    controller.abort();
  };
});

Solution

Voici une des solutions possibles de l’exercice :

useRef

useRef va renvoyer un objet modifiable qui n’impactera pas le cycle de vie du composant. La syntaxe est la suivante.

const refContainer = useRef(initialValue);
  • refContainer : Objet avec une propriété current
  • initialValue : Optionnel, c’est la valeur donnée à la propriété current du retour de useRef

Référence de composant ou d’élément du DOM

L’usage le plus commun de cet Hook est la référence d’un composant.

import { useRef } from "react";

function InputWithFocus() {
  // Définition de la référence
  const inputRef = useRef();

  // Lors du clic sur le bouton, la fonction 'focus' de l'input sera appelée
  const onButtonClick = () => {
    // On utilise la propriété 'current' de la référence pour y accéder
    inputRef.current.focus();
  };

  return (
    <>
      {/* Initialisation de la référence */}
      <input ref={inputRef} type="text" />
      <button onClick={onButtonClick}>Focus</button>
    </>
  );
}
// ...

Variable mutable

Mais c’est surtout intéressant pour garder une variable modifiable qui peut être utilisée sans influencer le reste du comportement.

Ici, on va pouvoir utiliser l’id de l’intervalle pour pouvoir l’arrêter n’importe où dans le composant.

// ...
function Minuteur() {
  // Définition de la référence
  const intervalRef = useRef();
  const [timer, setTimer] = useState(30);

  useEffect(() => {
    const id = setInterval(() => {
      // On décrémente le minuteur géré par le state
      setTimer((oldTimer) => oldTimer - 1);
    }, 1000);

    // Mise à jour de la référence
    intervalRef.current = id;
    return () => {
      // Arrêt du ‘timer’ en cas de suppression ou réaffichage du composant
      clearInterval(intervalRef.current);
    };
  }, []);

  // Fonction permettant d'arrêter le ‘timer’
  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      {/* Le timer est affiché ici */}
      <p>Il reste : {timer} secondes</p>
      {/* En cliquant sur ce bouton, le minuteur (et donc l'intervalle) sera arrêté */}
      <button onClick={stopTimer}> STOP! </button>
    </div>
  );
}
// ...

Extérieur au cycle de vie

La modification de cette variable ne déclenchera pas de nouveau rendu.

Dans cette exemple, l’affichage du compteur restera à 0. De même, si des effets étaient en place, aucun d’entre eux ne se déclencheraient.

import React, { useRef } from "react";

function Exemple() {
  const counter = useRef(0);

  const addCount = () => {
    counter.current++;
  };

  return (
    <div>
      <p>Eeeeeeeeeeeet le compteur est actuellement égal à : {count}</p>
      <button onClick={addCount}>C'est ici qu'il faut cliquer</button>
    </div>
  );
}

L’exo

Dans cet exercice, on oublie le compteur. On va faire un lecteur audio ! Le but va être d’implémenter les boutons play et pause du lecteur. Il faudra utiliser les méthodes play et pause de l’élément audio.

L’Exercice : useRef - Exercice

Solution

Voici une des solutions possibles :

Conclusion

J’apprécie particulièrement la facilité d’utilisation et la nouvelle vision que ça induit dans la construction des applications.

Pour continuer de s’améliorer sur ces points, entrainez-vous. Il n’y a que ça qui fonctionne. Pour ma part, ça a commencé par la réécriture de certains composants écrits avec des classes. Puis, la création de nouveaux composants avec des Hooks. Avec les petits, comme les boutons, au début, puis, les vues ou les éléments plus complexe.

Je vous souhaite bonne chance pour la suite !

Références