PlongĂ©e dans les Hooks React 🩑

⏱ 12 minutes de lecture

PlongĂ©e dans les Hooks React 🩑

L’API de React est loin d’ĂȘtre figĂ©e et ne cesse d’évoluer. En tĂ©moigne l’une des derniĂšres nouveautĂ©s proposĂ©e par Sophie Alpert et Dan Abramov lors de la ReactConf 2018. : les hooks.

Je vous propose aujourd’hui d’effectuer une petite plongĂ©e Ă  la dĂ©couverte de ces mystĂ©rieuses crĂ©atures. Suivez le guide ! 🐟

Les Hooks

DĂ©couverte de la faune

đŸ—ș Localisation : React 16.7.0
🐡 Nombre de spĂ©cimens : 10

L’API des hooks est fonctionnelle mais reste en phase de proposition : placĂ©e dans l’agora communautaire, celle-ci peut encore Ă©voluer d’ici sa publication dans la version 16.7 (dont une version alpha est dĂ©jĂ  disponible). Les hooks introduisent une nouvelle maniĂšre d’utiliser les composants fonctionnels en leur donnant la capacitĂ© d’avoir un state et bĂ©nĂ©ficier de fonctionnalitĂ©s jusqu’ici rĂ©servĂ©es aux classes. Les hooks permettent maintenant de :

  • Partager de la logique stateful entre composants ;
  • RĂ©duire le boilerplate induit par les composants de classe et leurs mĂ©thodes de cycle de vie ;
  • Limiter l’utilisation des classes grĂące Ă  une approche fonctionnelle.

Ils sont au nombre de 10 :

import {
  // Basic Hooks 🐟
  useState,
  useEffect,
  useContext,
  // Additional Hooks 🩑
  useReducer,
  useCallback,
  useMemo,
  useRef,
  useImperativeMethods,
  useMutationEffect,
  useLayoutEffect,
} from 'react';

// 🐚 🩀

Les 3 premiers hooks sont les plus courants : ils sont le ciment de notre logique applicative, les autres hooks sont semblable à des helpers et s’avùrent aussi trùs utiles.

RÚgles de sécurité

Avant de plonger quelques rĂšgles de sĂ©curitĂ©. Les hooks sont d’une grande utilitĂ©, mais leur utilisation implique de respecter deux rĂšgles :

  • Ne pas les utiliser autre part que dans des fonctions React ;
  • Ne pas les utiliser dans des boucles, conditions et fonctions imbriquĂ©es.

Pour nous faciliter le respect de ces rĂšgles un plugin ESLint est disponible.

Matériel de plongée

Pour utiliser les hooks, il vous faudra installer la version alpha de React 16.7 (ou bien directement sur cette CodeSandbox):

yarn add react@next react-dom@next

Enfilez votre combinaison, chargez vos bouteilles d’oxygĂšne, nous plongeons dĂšs maintenant dans les profondeurs des hooks React ! âš“ïžđŸŠ‘

Hook useState

📒 Documentation officielle

Bonne nouvelle : les classes n’ont plus le monopole du state ! Certainement le hook le plus intĂ©ressant car il permet dorĂ©navant de crĂ©er un composant fonctionnel stateful :

import React, { useState } from 'react';

function App() {
  const [name, setName] = useState('Baptiste');
  const handleChangeName = e => setName(e.target.value);

  return (
    <form>
      <input value={name} onChange={handleChangeName} />
    </form>
  );
}

Le hook prend en paramùtre la valeur initiale (ici “Baptiste”) et retourne une paire de deux valeurs :

  • la valeur courante (name) ;
  • la fonction pour modifier cette valeur (setName).

La syntaxe utilisĂ©e ici const [name, setName] est appelĂ©e array destructuring et permet de rĂ©cupĂ©rer directement les valeurs sans passer par des index (elle donne aussi l’avantage de pouvoir nommer ses variables), et est donc Ă©quivalent Ă  ce code :

const nameState = useState('Baptiste');
const name = nameState[0];
const setName = nameState[1];

Vous pouvez bien sûr utiliser autant de hooks useState que vous le désirez :

import React, { useState } from 'react';

function App() {
  const [name, setName] = useState('Baptiste');
  const [email, setEmail] = useState(null);

  const handleChangeName = e => setName(e.target.value);
  const handleChangeEmail = e => setEmail(e.target.value);

  return (
    <form>
      <input value={name} onChange={handleChangeName} />
      <input value={email} onChange={handleChangeEmail} />
    </form>
  );
}

À noter que, tout comme la fonction this.setState, vous pouvez utiliser une fonction pour faire une mise Ă  jour qui dĂ©pend de la valeur prĂ©cĂ©dente (et ainsi Ă©viter des effets de bord) :

const [step, setStep] = useState(0);
const handleIncrement = e => setStep(step => step + 1);

Nous pouvons donc maintenant crĂ©er des composants fonctionnels stateful, mais ceux-ci sont pour l’instant limitĂ©s : nous n’avons pas accĂšs aux mĂ©thodes de cycle de vie.

Hook useEffect

📒 Documentation officielle

Ce hook permet d’ajouter notre logique comme nous le faisions avec les mĂ©thodes componentDidMount, componentDidUpdate et componentWillUnmount.

Souscrire Ă  un Ă©venement

Le hook est exĂ©cutĂ© aprĂšs chaque render du composant (premier compris) et permet donc de charger des donnĂ©es, interagir avec le DOM, etc
 Vous pouvez souscrire Ă  des Ă©vĂ©nements, par exemple rĂ©cupĂ©rer la largeur de la fenĂȘtre :

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

function App() {
  const [name, setName] = useState('Baptiste');
  const [email, setEmail] = useState(null);
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  const handleChangeName = e => setName(e.target.value);
  const handleChangeEmail = e => setEmail(e.target.value);

  // ✹ Notre hook useEffect ✹
  useEffect(() => {
    const handler = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', handler);
  });

  return (
    <form>
      <input value={name} onChange={handleChangeName} />
      <input value={email} onChange={handleChangeEmail} />
      <div>Largeur fenĂȘtre : {windowWidth}</div>
    </form>
  );
}

Clean up

Il reste un problĂšme avec l’implĂ©mentation ci-dessus : Ă  chaque mise Ă  jour, notre composant va rĂ©-Ă©couter notre Ă©vĂ©nement, ainsi le handler sera appelĂ© autant de fois que le composant s’est rendu !

Heureusement, il est possible de retourner une fonction dans votre hook : celle-ci sera exécutée avant chaque nouveau effect (et lorsque le composant est supprimé du DOM). Cela vous permet de supprimer votre listener :

useEffect(() => {
  const handler = () => {
    setWindowWidth(window.innerWidth);
  };

  window.addEventListener('resize', handler);

  // Clean up
  return function cleanup() {
    window.removeEventListener('resize', handler);
  };
});

GĂ©rer plus finement vos effects

Dernier dĂ©tail qui a son importance : vous pouvez passer un tableau de variable en deuxiĂšme argument, le hook ne sera alors appelĂ© que si l’un de ses arguments change. Cela peut ĂȘtre trĂšs utile pour appeler une API si une props change afin de rafraĂźchir les donnĂ©es :

function App({ username }) {
  const [user, setUser] = useState(null);

  useEffect(
    () => {
      fetch(`https://api.github.com/users/${username}`)
        .then(response => response.json())
        .then(data => {
          setUser(data);
        });
    },
    [username],
  );

  return <section>{user && user.email}</section>;
}

Ici, le hook sera appelé uniquement lorsque la props username passée à notre composant changera.

Vous pouvez paramĂ©trer votre hook afin qu’il s’exĂ©cute une seule fois (semblable Ă  un componentDidMount donc) en lui passant un tableau vide :

useEffect(() => {
  // code
}, []);

Enfin, tout comme le hook useState, vous pouvez l’appeler autant de fois que vous voulez.

Créer ses propres hooks

L’une des grandes forces des hooks est de pouvoir les extraire dans ses propres fonctions et crĂ©er ses propres hooks. Ainsi nous pouvons reprendre toute la logique ajoutĂ©e plus haut et la dĂ©placer dans une simple fonction (qui par convention doit commencer par use) :

import React, { useState } from 'react';

function App() {
  const windowWidth = useWindowWidth();

  return <div>Largeur : {windowWidth}</div>;
}

function useWindowWidth() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handler = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', handler);
  });

  return windowWidth;
}

Il devient alors facile d’utiliser cette mĂȘme logique dans d’autres composants : cela Ă©tait dĂ©jĂ  possible avec des patterns comme les render props, mais les hooks React apportent une API plus simple et concise.

Cette nouvelle possibilitĂ©, a trĂšs vite inspirĂ© la communautĂ©, qui n’a pas attendu pour sortir des collections de hooks. Il en existe dĂ©jĂ  des dizaines :

Les librairies react-use, react-powerhooks, react-hanger, rehooks, the-platform proposent toutes des collections de hooks, prĂȘtes Ă  l’emploi. Certains sites comme hooks.guide centralise l’ensemble des hooks.

La communautĂ© n’a donc pas attendu la version 16.7 pour s’y intĂ©resser !

Les autres hooks

Nous avons vu les deux hooks les plus importants, mais la nouvelle API recĂšle d’autres hooks trĂšs utiles. PrĂ©sentation.

Hook useContext

📒 Documentation officielle

La version 16.6 de React apportait dĂ©jĂ  une simplification de l’API pour les classes, ce hook facilite encore plus l’utilisation du context au sein d’un composant fonctionnel :

const context = useContext(Context);

A chaque fois que le provider sera modifié, le hook provoquera un nouveau rendu.

Hook useCallback

📒 Documentation officielle

Vous avez du lire des dizaines de fois : inliner des fonctions dans vos composants peut provoquer des problĂšmes de performance. En effet, lors du rendu de votre composant une nouvelle fonction sera crĂ©Ă©e Ă  chaque fois, empĂȘchant de bloquer les rendus inutiles. En utilisant le hook useCallback plus de soucis :

const handleOnSubmit = useCallback(
  () => {
    // code
  },
  [a, b],
);

Ce hook permet de toujours passer la mĂȘme instance de votre fonction si les paramĂštres ne changent pas. Plus besoin donc de crĂ©er une classe pour sortir les fonctions de votre mĂ©thode render !

Hook useMemo

📒 Documentation officielle

Ce hook permet de mĂ©moiser une fonction, c’est Ă  dire crĂ©er une sorte de cache afin de l’exĂ©cuter uniquement si les arguments ont changĂ©s. Si la fonction a dĂ©jĂ  Ă©tĂ© exĂ©cutĂ©e avec les arguments donnĂ©s, elle retournera directement la valeur sauvegardĂ©e. Cela peut amĂ©liorer les performances de vos composants en cas de fonctions gourmandes.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Hook useRef

📒 Documentation officielle

Ce hook permet de créer une référence dans votre composant fonctionnel, pour par exemple focus un champs :

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

  useEffect(() => {
    inputElement.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputElement} type="text" />
    </div>
  );
}

Hook useReducer

📒 Documentation officielle

Permet d’embarquer un mini Redux dans vos composants, ce hook utilise une API semblable Ă  Redux en retournant un state et une fonction de dispatch pour modifier votre store via un reducer. Cela peut s’avĂ©rer trĂšs pratique pour gĂ©rer des states complexes ou des machines Ă  Ă©tats.

A noter que ce hook accepte un troisiĂšme argument permettant de dĂ©finir une action d’initialisation (jouĂ©e lors du premier rendu).

Voici une implĂ©mentation minimaliste de Redux avec ce hook et l’utilisation du contexte (disponible aussi sur cette CodeSandbox) :

// Redux
const StateContext = React.createContext(null);
// Nous utilisons un contexte séparé pour le dispatch car cette fonction n'est jamais modifié, nous évitons ainsi des rendus si un composant ne nécessite pas le state.
const DispatchContext = React.createContext(null);
const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return { count: action.payload };
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
  }
}

// Composant lié au state et a la fonction de dispatch
function DeepDecrement() {
  const state = useContext(StateContext);
  const dispatch = useContext(DispatchContext);
  return <button onClick={() => state.count > 0 && dispatch({ type: 'decrement' })}>Decrement</button>;
}

// Composant uniquement lié au dispatch
function DeepIncrement() {
  const dispatch = useContext(DispatchContext);
  return <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>;
}

// Composant d'application avec initialisation du reducer et création des providers pour le contexte
function App() {
  const initialCount = 0;
  const [state, dispatch] = useReducer(reducer, initialState, {
    type: 'reset',
    payload: initialCount,
  });

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        <div className="App">
          <p>{state.count}</p>
          <DeepDecrement />
          <DeepIncrement />
        </div>
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Fin de la plongée !

Remontons Ă  la surface pour reprendre un peu d’air ! Nous le rappelons : cette API est toujours en phase de proposition. Il n’est pas recommandĂ© de rĂ©-Ă©crire toutes vos applications avec ce pattern pour l’instant, mais il est intĂ©ressant de commencer Ă  l’expĂ©rimenter sur vos nouveaux composants !

Comme le prĂ©cise Dan Abramov dans son article, la communautĂ© s’est rapidement emparĂ© des hooks, mais mĂȘme pour leurs crĂ©ateurs il reste des zones d’ombre :

💜

Notre veille sur le sujet

Continuer la discussion sur Twitter