Les hooks React (1/3)

Avant-propos : des nouvelles de ce blog

Ça faisait des années que j’avais laissé ce blog pour mort, il est temps de le sortir de sa torpeur !

Je contribuais principalement sur byteclub.fr mais cet épisode étant terminé et comme on a lamentablement laissé mourir le domaine et le site, je vais rapatrier ici les articles. Et continuer à en produire ici-même 🙂 à commencer par une série de 3 articles pour faire le tour des hooks React, et la traduction d’un article complet sur le server-side rendering.

Dans le vif du sujet : alors c’est quoi ces hooks ?

Les hooks (lire l’introduction officielle) sont une des grosses nouveautés de React, une de celle qui a généré une ébulition dans la communauté comme rarement. Et pour cause : elle permet d’accéder dans les composants « fonction » à des fonctionnalités jusqu’ici réservées aux classes.

Nous allons dans cet article découvrir les hooks, comprendre leur intérêt, et en manipuler quelques-uns autour d’un exemple simple. Avant ça, commençons par éclaircir quelques points :

  • Ce que les hooks permettent : ils donnent la possibilité créer des composants « fonction » avec un accès complet aux fonctions internes d’un composant et pas seulement son rendu, et ils réduisent la quantité de code nécessaire par rapport à la version « classe » ;
  • Ce que les hooks ne permettent pas : ils n’offrent aucune fonctionnalité supplémentaire par rapport aux classes ;

Problèmes et limitations

Commençons par les quelques bugs qui traînent : vous vous doutez qu’avec le sérieux de l’équipe de React il y en a fort peu qui sont passés au travers des mailles du filet des béta-tests. Il en reste néanmoins un, non bloquant mais un peu pénible : lorsqu’on écrit des tests utilisant react-dom/test-utils (ce qui inclut par exemple react-testing-library) si vous testez un composant qui utilise le hook useState et modifie son état dans une opération asynchrone (typiquement une mise à jour de l’état après un appel d’API) alors vous ne pourrez pas vous débarrasser d’un warning à propos de la fonction act(). Vous pouvez l’ignorer pour l’instant, tout ça est déjà réglé dans la future version 16.9.0 de react, et également dans la version de react-testing-library qui suivra juste après.

Quant aux limites des hooks elles sont assez simples et inhérentes au choix de conserver une API très simple : afin de faire le lien entre l’instance interne du composant, et le hook, React n’a guère d’options et l’équipe a donc choisi de simplement les faire concorder par l’ordre de déclaration. Ainsi, si lors d’un premier rendu du composant vous utilisez deux hooks, puis trois dans le rendu suivant, React ne saura pas faire le lien et sera totalement perdu. Ainsi, les hooks devraient uniquement être déclarés en tête de fonction, et surtout sans condition ni boucle ! Une règle eslint assure que vous serez prévenu si vous faites cette erreur, n’hésitez pas à l’ajouter dans vos nouveaux projets ! Consultez le chapitre dédié de la doc officielle pour en savoir plus.

Pour commencer : useState

Prenons un bouton qui incrémente son libellé au clic, sa version « classe », avec les raccourcis permis par la syntaxe moderne débloquée par Babel (ici le destructuring assignment, les default values, les static members, ainsi que les public class fields pour le state et le callback) :

Éditer dans JSBin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Button extends React.Component {
  static defaultProps = { initialValue: 1 }
 
  state = { value: this.props.initialValue }
 
  increment = () => {
    this.setState(state => ({ value: state.value + 1 }))
  }
 
  render() {
    return (      
      <button>{this.state.value}</button>      
    );
  }
}

Maintenant, découvrons ensemble le premier hook, le plus classique, celui permettant d’accéder à l’état de l’instance interne du composant. Il s’appelle useState, prend en paramètre la valeur initiale, et retourne un couple constitué de la valeur initiale et d’une fonction modifiant cette valeur. L’appel à cette fonction va mettre à jour l’état de l’instance de ce composant, et provoquer un nouveau rendu (ce qui aura pour effet de rappeler useState, qui renverra donc la nouvelle valeur) :

Éditer dans JSBin
1
2
3
4
5
6
7
8
const Button = ({ initialValue = 1 }) => {
  const [value, setValue] = React.useState(initialValue)
  const increment = () => setValue(value + 1)
 
  return (      
    <button onClick={increment}>{value}</button>      
  )
}

Comme promis, c’est (deux fois) plus court 🙂 Le résultat est le même (à un petit détail près que je détaillerai plus tard, au niveau de la déclaration du callback). Attention il est important de noter que ce hook gère sa valeur de manière atomique : si vous avez un état composé de plusieurs variables, vous devrez faire autant d’appels à useState, ou bien vous devrez reconstruire la valeur complète car le setter retourné ne fait pas de mise à jour partielle comme setState. La convention est de gérer chaque portion de state dans un un appel à useState dédié.

Pour optimiser : useCallback et useMemo

Il est d’usage afin d’éviter de provoquer des rendus inutiles, de « garder » les callbacks sous forme de membre d’instance : ainsi l’expression n’est calculée qu’une fois et c’est bien la même référence qui est passée en prop à chaque fois. Exemple :

/**
 * Dans ce composant, à chaque rendu, on génère une nouvelle fonction
 * de callback, ainsi SubComponent reçoit une prop différente et même
 * si c'est un PureComponent, il sera re-rendu inutilement
 */
class Component1 extends Component {
  render() {
    const callback = () => this.setState()
    return <subcomponent onClick={callback}></subcomponent>
  }
}
 
/**
 * En revanche dans ce composant, on a "mis de côté" le callback à
 * la création, et à chaque rendu on passe la même référence en prop
 * ainsi si SubComponent est un PureComponent, il ne sera pas re-rendu
 */
class Component2 extends Component {
  constructor(props) {
    super(props)
    this.callback = () => this.setState()
  }
  render() {
    return <subcomponent onClick={this.callback}></subcomponent>
  }
}
 
/**
 * À noter que l'exemple précédent peut être écrit plus simplement
 * grâce aux "public class fields" qu'on a utilisé plus haut
 */
class Component2_shorter extends Component {
  callback = () => this.setState()
  render() {
    return <subcomponent onClick={this.callback}></subcomponent>
  }
}

Cette optimisation est importante pour éviter les rendus intermédiaires inutiles, or si on reprend notre exemple en version « fonction + hooks » plus haut, on voit bien qu’on génère une nouvelle variable increment à chaque rendu. Il n’est évidemment pas possible de simplement déclarer cette fonction dans le scope supérieur, donc il faudrait pouvoir « l’attacher » à l’instance du composant. C’est l’objet de useCallback : ce hook va prendre en paramètre une fonction et un éventuel « tableau de dépendances » et retourner la fonction correspondante. L’intérêt est que si les valeurs listées dans le tableau ne changent pas, alors il retourne la même référence. Dans notre exemple de compteur ça donnerait ça :

Éditer dans JSBin
const Button = ({ initialValue = 1 }) => {
  const [value, setValue] = React.useState(initialValue)
  const increment = React.useCallback(() => setValue(value + 1), [value, setValue])
 
  return (
    <button onClick={increment}>{value}</button>
  )
}

Il y a plusieurs choses à noter sur ce hook et cet exemple :

  • Si on ne spécifie pas le tableau de dépendances, alors une nouvelle valeur est calculée à chaque rendu, et donc l’utilisation de ce hook est strictement inutile ;
  • Si on spécifie un tableau vide, la fonction n’est calculée qu’à la création de l’élément, et jamais recalculée durant sa vie (ainsi le compteur sera bloqué à 2 car le callback n’aura gardé que la référence à value = 1) ;
  • Le plus simple et efficace est de systématiquement passer toutes les variables référencées depuis la fonction dans le tableau de dépendances (la règle eslint exhaustive-deps sert à vous y aider) ;
  • Il est intéressant de noter que useState retourne un setter unique pour toute la vie du composant (la référence sera toujours la même), donc on n’est pas obligé de le passer au tableau de dépendances, néanmoins autant être exhaustif ;
  • Dans cet exemple, l’utilisation de useCallback est de toute façon inutile puisque la seule cause de re-rendu est le changement de value qui provoquera donc une nouvelle référence… mais c’est le lot des exemples triviaux 😉

useMemo fonctionnera de la même manière, mais exécutera la fonction qu’on lui passe pour retourner la valeur calculée. Cette valeur ne sera ainsi recalculée que si une des dépendances change de valeur.

Et la suite ?

Ça nous a fait déjà pas mal de lecture, donc la suite au prochain épisode ! Au menu :

  • Les différentes utilisations de useEffect et useRef ;
  • L’écriture de hooks personnalisés ainsi que l’utilisation de useDebugValue ;

Enfin, dans le troisième article de cette série, nous aborderons useContext et useReducer en réimplémentant un Redux maison.

À l’issue de cette série de trois articles, vous aurez eu une explication complète et illustrée des hooks principaux. Personnellement je suis spécialement fan de useEffect qui apporte vraiment une simplification du code, mais chut, pas de spoiler 🙂


Sommaire de la série

  1. « Les hooks React (1/3) » où l’on parle des hooks en général et de useState et useCallback en particulier.
  2. « Les hooks React (2/3) » où l’on parle de la gestion des effets et de leur réutilisation sans pattern compliqué avec useEffect.
  3. « Les hooks React (3/3) » où l’on parle des contextes et où on essaie de faire du mal à Redux.

2 réflexions sur « Les hooks React (1/3) »

  1. Ping : Les hooks React (2/3) | naholyr.fr

  2. Ping : Les hooks React (3/3) | naholyr.fr

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.