Depuis la fin du mois d’octobre et la présentation donnée à la React Conf par Dan Abramov, toute la sphère React était en effervescence après l’annonce des Hooks. Bien que ces derniers n’ajoutent pas de nouvelles fonctionnalités à proprement parler, ils nous obligent cependant à repenser entièrement notre façon de développer…

Quelques rappels sur React

Créé en 2013 React est, à la différence d’Angular, une librairie permettant de créer des composants d’un site ou d’une application web. Ces composants sont utilisés pour façonner le plus simplement possible des UI parfois complexes.

Il existe ainsi 2 façons d’écrire un composant

Fonction

import React from 'react';

function Welcome() {
  return <div>Hello, world !</div>;
}

Les fonctions sont la façon la plus simple d’écrire un composant, elles ne possèdent pas ou très peu de logiques et retournent un élément JSX qui sera par la suite affiché dans l’arbre DOM.

Class

import React, { Component } from 'react';

class Welcome extends Component {
  render() {
    return <div>Hello, world !</div>;
  }
}

Les classes nous permettent une plus grande liberté, elles ont accès au contexte et aux méthodes du lifecycle. Il est donc possible d’y introduire plus de logique comme des appels API par exemple

import React, { Component } from 'react';
import { fetchData } from './api';

class Welcome extends Component {
 constructor(props) {
   super(props);
   this.state = {
     data: {}
   }
 }

 componentDidMount() {
   fetchData().then(data => this.setState({ data }));
 }

 render() {
   return <div>{this.state.data}</div>;
 }
}

Problématiques

Le modèle de React avant la version des Hooks (16.8.0) pose plusieurs soucis, en effet dans certains cas comme nous avons pu le voir un peu plus haut nous sommes dans l’obligation d’utiliser une classe plutôt qu’une fonction. Et cela soulève quelques points d’améliorations :

  • Performance : Même si ce n’est que mineur voire très mineur, les fonctions sont en React plus performantes que les classes car elles n’implémentent de base aucunes méthodes du lifecycle.
  • Lisibilité : En transformant notre fonction en classe, nous augmentons parfois de façon significative notre nombre de ligne. Et malheureusement cela se traduit par une baisse de la lisibilité du code.
  • Réutilisation : C’est le plus gros problème, à l’heure actuelle il est très complexe d’éviter la duplication de code au sein de plusieurs composants, voire même au sein d’un même composant !

Voici un exemple pour illustrer

import React, { Component } from 'react';

class Welcome extends Component {
 constructor(props) {
   super(props);
   this.state = {
    value: ''
   }
 
   this.handleChange = this.handleChange.bind(this);
 }

 componentDidMount() {
   document.title = this.state.value;
 }

 componentDidUpdate() {
   document.title = this.state.value;
 }

 handleChange(event) {
   this.setState({ value: event.target.value });
 }

 render() {
   return <div><input value={this.state.value} onChange={this.handleChange}/></div>;
 }
}

Ici nous avons simplement un input qui met à jour le titre dans notre onglet et nous nous retrouvons avec toutes les problématiques évoquées plus haut :

  • Utilisation d’une classe pour une fonctionnalité simple
  • Duplication de code (dans le componentDidMount et componentDidUpdate)
  • La logique n’est pas réutilisable à l’extérieur de ce composant
  • Il y a au final beaucoup de lignes pour pas grand chose…

Présentation des Hooks

Les Hooks au sens React sont des fonctions qui sont exécutées à l’intérieur d’autres fonctions (donc de composants) afin de modifier leurs comportements. Ce terme n’est pas propre à React, il est également utilisé en Angular et en Vue, pour désigner les méthodes du cycle de vie.

Voici la différence de signification des Hooks :

  • React : fonction utilisée uniquement dans d’autres fonctions.
  • Angular/Vue : Méthode du cycle de vie (Lifecycle Hooks sur la documentation officielle) utilisée uniquement dans des classes.

Ces Hooks vont donc nous permettre de modifier nos fonctions pour leur permettre de se comporter presque comme des classes. En effet il existera toujours quelques petites différences, si vous souhaitez approfondir le sujet vous pouvez consulter cette page.

Exemple de Hooks

UseState

Parfois nous devons faire évoluer notre composant, déclaré au préalable sous forme de fonction, et nous nous rendons compte que nous allons avoir besoin d’un state. Avant la release 16.8.0, le seul moyen d’y parvenir était de transformer notre fonction en classe. Il est maintenant possible de faire tout cela dans notre fonction ! Voici un exemple :

import React, { useState } from 'react';

function useStateExample() {
  const [showPanel, setShowPanel] = useState(false);

  function onButtonClick() {
    setShowPanel(true);
  }

  if(showPanel) {
    return <div>Affichage du panel</div>;
  }

  return <button onClick={onButtonClick}>Afficher le panel ? </button>;
}

Une ligne inhabituelle vient de faire son apparition :

const [showPanel, setShowPanel] = useState(false);

La fonction useState permet de déclarer une variable au sein d’un composant de type fonction. Elle renvoie un tableau à 2 éléments, le 1e étant le nom de la variable et le 2e le modificateur de celle-ci. Elle prend également une valeur qui sera la valeur initiale de notre variable. Ainsi au 1e rendu, React interprétera notre composant comme ceci :

import React from 'react';

function useStateExample() {
 const showPanel = false;
 // const [showPanel, setShowPanel] = useState(false);

 function onButtonClick() {
   setShowPanel(true);
 }

 if(showPanel) {
   return <div>Affichage du panel</div>;
 }

 return <button onClick={onButtonClick}>Afficher le panel ? </button>;
}

UseEffect

Nous avons précédemment que nous pouvions avoir du doublon de code :

import React, { Component } from 'react';

class Welcome extends Component {
 constructor(props) {
   super(props);
   this.state = {
     value: ''
   }
 
   this.handleChange = this.handleChange.bind(this);
 }

 componentDidMount() {
   document.title = this.state.value;
 }

 componentDidUpdate() {
   document.title = this.state.value;
 }

 handleChange(event) {
   this.setState({ value: event.target.value });
 }

 render() {
   return <div><input value={this.state.value} onChange={this.handleChange}/></div>;
 }
}

Dans un 1e temps, transformons cette classe en fonction en utilisant la fonction useState vu dans la section précédente :

import React, { useState } from 'react';

function Welcome() {
  const [value, setValue] = useState('');

  function handleChange(event) {
    setValue(event.target.value);
  }

  return <div><input value={value} onChange={handleChange}/></div>;
}

Nous devons maintenant gérer l’affichage de notre onglet en fonction de notre input. Pour cela nous allons utiliser la fonction useEffect qui sera appelée à chaque fois qu’une modification est détectée sur notre composant.

import React, { useState, useEffect } from 'react';

function Welcome() {
 const [value, setValue] = useState('');

 function handleChange(event) {
   setValue(event.target.value);
 }

 useEffect(() => {
  document.title = value;
 });

 return <div><input value={value} onChange={handleChange}/></div>;
}

Cette notation est correcte mais présente néanmoins un problème. Pour s’en rendre compte nous allons ajouter une nouvelle fonctionnalité à notre composant. Celui-ci va maintenant réagir et afficher la taille de la fenêtre courante.

import React, { useState, useEffect } from 'react'; 

function Welcome() { 
  const [value, setValue] = useState(''); 

  function handleChange(event) { 
    setValue(event.target.value); 
  } 

  useEffect(() => { 
    document.title = value; 
  });

  // Gestion de la taille de l'écran
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    }
  }); 

  return (<div>
           <input value={value} onChange={handleChange}/>
           {width}
         </div>); 
  }

Avant de parler de la problématique en elle même, attardons-nous un peu sur les nouvelles lignes.

const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
 // Simule un componentDidMount et componentDidUpdate
 const handleResize = () => setWidth(window.innerWidth);
 window.addEventListener('resize', handleResize);

 return () => {
   // Simule un componentDidUnmount
   window.removeEventListener('resize', handleResize);
 }
});

Ce useEffect est un peu différent du précédent dans le sens où il retourne quelque chose. Cette valeur de retour sert uniquement à jouer le rôle de destructeur de notre composant, ainsi lorsque ce dernier sera détruit le listener sera supprimé.

Nous pouvons maintenant nous attarder sur notre problématique, en effet nous avons 2 fonctions useEffect exécutées à chaque modification du composant. Dit en d’autres mots, notre composant va exécuter l’entièreté des useEffect et ce même si ces derniers ne sont à priori pas concernés par des modifications. Dit encore autrement, lorsque nous touchons la taille de notre fenêtre, le code se trouvant à l’intérieur du useEffet concernant le titre sera également exécuté…

Evidemment il existe une solution toute simple à ce problème, il s’agit de rajouter un second paramètre à notre fonction useEffect. Ce paramètre est un tableau des variables sur lesquelles notre useEffect va réagir et donc appliquer des modifications. Nous pouvons donc compléter notre code :

import React, { useState, useEffect } from 'react'; 

function Welcome() { 
 const [value, setValue] = useState(''); 

 function handleChange(event) { 
   setValue(event.target.value); 
 } 

 useEffect(() => { 
   document.title = value; 
 }, [value]);

 // Gestion de la taille de l'écran
 const [width, setWidth] = useState(window.innerWidth);
 useEffect(() => {
   const handleResize = () => setWidth(window.innerWidth);
   window.addEventListener('resize', handleResize);

   return () => {
     window.removeEventListener('resize', handleResize);
   }
 }, [width]); 

   return (<div>
            <input value={value} onChange={handleChange}/>
            {width}
          </div>); 
 }

Avec ce second paramètre il est maintenant possible de simuler uniquement un componantDidMount, pour cela il suffit simplement de renseigner un tableau vide.

import React, { useState, useEffect } from 'react';
import { fetchData } from './api';

function Welcome() {
  const [data, setData] = useState({});

  useEffect(() => {
    fetchData().then(data => setData(data));
  }, []);

  return <div>{data}</div>
}

Si vous souhaitez approfondir sur ce sujet vous pouvez regarder cette article.

UseContext

Les Context ont été introduit dans la version 16.3.0, ils ont pour but d’éviter de passer de nombreuses props dans un arbre donné. On peut créer des Context pour tout et n’importe quoi, comme par exemple en remplacement de Redux si la logique est assez basique. Les cas les plus connus et les plus courants de Context sont la gestion du thème principal et de la langue courante utilisée dans l’application.

Voici comment les Context sont utilisés avant la version 16.8.0.

import React from 'react';
import ThemeContext from './themeContext';
import LocaleContext from './localeContext';

function Welcome() {
  return (
   <ThemeContext.Consumer>
   {theme => (
     <div className={theme}>
     <LocaleContext.Consumer>
       {locale => (
         <span>{locale}</span>
       )}
     </LocaleContext.Consumer>
     </div> 
   )}
   </ThemeContext.Consumer>
  );
}

Nous pouvons voir ici que la complexité est assez importante alors que nous souhaitons simplement afficher 2 valeurs issues de 2 Context différents. L’introduction des hooks permet donc de grandement simplifier cette écriture.

import React, { useContext } from 'react';
import ThemeContext from './themeContext';
import LocaleContext from './localeContext';

function Welcome() {
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);


  return (
   <div className={theme}>
     <span>{locale}</span>
   </div>
  )
}

Pour aller plus loin

Nous avons vu ici comment utiliser les 3 principaux Hooks : useState, useEffect et useContext, il en existe plusieurs autres qui peuvent se révéler très utiles en fonction des cas rencontrés. Vous trouverez toute la documentation nécessaire ici.