Réutiliser de la logique grùce aux Hooks personnalisés

React fournit plusieurs Hooks tels que useState, useContext et useEffect. Parfois, vous aimeriez qu’il y ait un Hook pour un besoin plus prĂ©cis : par exemple pour rĂ©cupĂ©rer des donnĂ©es, savoir si un utilisateur est en ligne ou encore se connecter Ă  un salon de discussion. Vous ne trouverez peut-ĂȘtre pas ces Hooks dans React, mais vous pouvez crĂ©er vos propres Hooks pour les besoins de votre application.

Vous allez apprendre

  • Ce que sont les Hooks personnalisĂ©s et comment Ă©crire les vĂŽtres
  • Comment rĂ©utiliser de la logique entre composants
  • Comment nommer et structurer vos Hooks personnalisĂ©s
  • Quand et pourquoi extraire des Hooks personnalisĂ©s

Hooks personnalisĂ©s : partager de la logique entre composants

Imaginez que vous dĂ©veloppiez une appli qui repose massivement sur le rĂ©seau (comme c’est le cas de la plupart des applis). Vous souhaitez avertir l’utilisateur si sa connexion rĂ©seau est brutalement interrompue pendant qu’il utilisait son appli. Comment vous y prendriez-vous ? Il semble que vous ayez besoin de deux choses dans votre composant :

  1. Un Ă©lĂ©ment d’état qui dĂ©termine si le rĂ©seau est en ligne ou non.
  2. Un effet qui s’abonne aux Ă©vĂ©nements globaux online et offline, et met Ă  jour cet Ă©tat.

Ça permettra Ă  votre composant de rester synchronisĂ© avec l’état du rĂ©seau. Vous pouvez commencer par quelque chose comme ceci :

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ En ligne' : '❌ DĂ©connectĂ©'}</h1>;
}

Essayez d’activer et de dĂ©sactiver votre rĂ©seau et voyez comme cette StatusBar se met Ă  jour en fonction de vos actions.

Imaginez maintenant que vous souhaitiez utiliser la mĂȘme logique dans un composant diffĂ©rent. Vous voulez crĂ©er un bouton Enregistrer qui sera dĂ©sactivĂ© et affichera « Reconnexion
 Â» au lieu de « Enregistrer Â» lorsque le rĂ©seau est dĂ©sactivĂ©.

Pour commencer, vous pouvez copier-coller l’état isOnline et l’effet dans le SaveButton :

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progression enregistrĂ©e');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Enregistrer la progression' : 'Reconnexion...'}
    </button>
  );
}

VĂ©rifiez que le bouton changera d’apparence si vous dĂ©branchez le rĂ©seau.

Ces deux composants fonctionnent bien, mais la duplication de la logique entre eux est regrettable. Il semble que mĂȘme s’ils ont un aspect visuel diffĂ©rent, ils rĂ©utilisent la mĂȘme logique.

Extraire votre Hook personnalisĂ© d’un composant

Imaginez un instant que, comme pour useState et useEffect, il existe un Hook prĂ©dĂ©fini useOnlineStatus. Ces deux composants pourraient alors ĂȘtre simplifiĂ©s et vous pourriez supprimer la duplication entre eux :

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ En ligne' : '❌ DĂ©connectĂ©'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progression enregistrĂ©e');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Enregistrer la progression' : 'Reconnexion...'}
</button>
);
}

MĂȘme si un tel Hook intĂ©grĂ© n’existe pas, vous pouvez l’écrire vous-mĂȘme. DĂ©clarez une fonction appelĂ©e useOnlineStatus et dĂ©placez-y tout le code dupliquĂ© des composants que vous avez Ă©crits plus tĂŽt :

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

À la fin de la fonction, retournez isOnline. Ça permet Ă  votre composant de lire cette valeur :

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ En ligne' : '❌ DĂ©connectĂ©'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progression enregistrĂ©e');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Enregistrer la progression' : 'Reconnection...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

VĂ©rifiez que l’activation et la dĂ©sactivation du rĂ©seau mettent Ă  jour les deux composants.

DĂ©sormais, vos composants n’ont plus de logique dupliquĂ©e. Plus important encore, le code qu’ils contiennent dĂ©crit ce qu’ils veulent faire (utiliser le statut de connexion) plutĂŽt que la maniĂšre de le faire (en s’abonnant aux Ă©vĂ©nements du navigateur).

Quand vous extrayez la logique dans des Hooks personnalisĂ©s, vous pouvez masquer les dĂ©tails de la façon dont vous traitez avec des systĂšmes extĂ©rieurs ou avec une API du navigateur. Le code de vos composants exprime votre intention, pas l’implĂ©mentation.

Les noms des Hooks commencent toujours par use

Les applications React sont construites Ă  partir de composants. Les composants sont construits Ă  partir des Hooks, qu’ils soient prĂ©-fournis ou personnalisĂ©s. Vous utiliserez probablement souvent des Hooks personnalisĂ©s créés par d’autres, mais vous pourrez occasionnellement en Ă©crire un vous-mĂȘme !

Vous devez respecter les conventions de nommage suivantes :

  1. Les noms des composants React doivent commencer par une majuscule, comme StatusBar et SaveButton. Les composants React doivent également renvoyer quelque chose que React sait afficher, comme un bout de JSX.
  2. Les noms des Hooks doivent commencer par use suivi d’une majuscule, comme useState (fourni) ou useOnlineStatus (personnalisĂ©, comme plus haut dans cette page). Les Hooks peuvent renvoyer des valeurs quelconques.

Cette convention garantit que vous pouvez toujours examiner le code d’un composant et repĂ©rer oĂč son Ă©tat, ses effets et d’autres fonctionnalitĂ©s de React peuvent « se cacher Â». Par exemple, si vous voyez un appel Ă  la fonction getColor() dans votre composant, vous pouvez ĂȘtre sĂ»r·e qu’il ne contient pas d’état React car son nom ne commence pas par use. En revanche, un appel de fonction comme useOnlineStatus() contiendra trĂšs probablement des appels Ă  d’autres Hooks.

Remarque

Si votre linter est configurĂ© pour React, il appliquera cette convention de nommage. Remontez jusqu’au bac Ă  sable et renommez useOnlineStatus en getOnlineStatus. Remarquez que le linter ne vous permettra plus appeler useState ou useEffect Ă  l’intĂ©rieur. Seuls les Hooks et les composants peuvent appeler d’autres Hooks !

En détail

Toutes les fonctions appelĂ©es pendant le rendu doivent-elles commencer par le prĂ©fixe use ?

Non. Les fonctions qui n’appellent pas des Hooks n’ont pas besoin d’ĂȘtre des Hooks.

Si votre fonction n’appelle aucun Hook, Ă©vitez d’utiliser le prĂ©fixe use. À la place, Ă©crivez une fonction normale sans le prĂ©fixe use. Par exemple, useSorted ci-dessous n’appelle pas de Hook, appelez-la plutĂŽt getSorted :

// 🔮 À Ă©viter : un Hook qui n’utilise pas d’autres Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Correct : une fonction normale qui n’utilise pas de Hook
function getSorted(items) {
return items.slice().sort();
}

Ça garantit que votre code peut appeler cette fonction n’importe oĂč, y compris dans des conditions :

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ On peut appeler getSorted() conditionnellement parce qu’il ne s’agit pas d’un Hook
displayedItems = getSorted(items);
}
// ...
}

Vous devez utiliser le prĂ©fixe use pour une fonction (et ainsi, en faire un Hook) si elle utilise elle-mĂȘme un Hook dans son code :

// ✅ Correct : un Hook qui utilise un autre Hook
function useAuth() {
return useContext(Auth);
}

Techniquement, cette rĂšgle n’est pas vĂ©rifiĂ©e par React. En principe, vous pouvez crĂ©er un Hook qui n’appelle pas d’autres Hooks. C’est souvent dĂ©routant et limitant, aussi est-il prĂ©fĂ©rable d’éviter cette approche. Cependant, il peut y avoir de rares cas oĂč c’est utile. Par exemple, votre fonction n’appelle pas encore de Hook, mais vous prĂ©voyez d’y ajouter des appels Ă  des Hooks Ă  l’avenir. Il est alors logique d’utiliser le prĂ©fixe use :

// ✅ Correct : un Hook qui utilisera probablement des Hooks par la suite.
function useAuth() {
// TODO : remplacer cette ligne quand l’authentification sera implĂ©mentĂ©e :
// return useContext(Auth);
return TEST_USER;
}

Les composants ne pourront pas l’appeler de maniĂšre conditionnelle. Ça deviendra important quand vous ajouterez des appels Ă  des Hooks Ă  l’intĂ©rieur. Si vous ne prĂ©voyez pas d’appeler des Hooks dans votre fonction (ni maintenant ni plus tard), alors n’en faites pas un Hook.

Les Hooks personnalisĂ©s vous permettent de partager la logique d’état, mais pas l’état lui-mĂȘme

Dans l’exemple prĂ©cĂ©dent, lorsque vous avez activĂ© et dĂ©sactivĂ© le rĂ©seau, les deux composants se sont mis Ă  jour ensemble. Cependant, ne croyez pas qu’une seule variable d’état isOnline est partagĂ©e entre eux. Regardez ce code :

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Ça fonctionne de la mĂȘme façon qu’avant la suppression de la duplication :

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

Il s’agit de deux variables d’état et effets totalement indĂ©pendants ! Il se trouve qu’ils ont la mĂȘme valeur au mĂȘme moment parce que vous les avez synchronisĂ©s avec la mĂȘme donnĂ©e extĂ©rieure (le fait que le rĂ©seau est actif ou non).

Pour mieux illustrer ça, nous allons avoir besoin d’un exemple diffĂ©rent. Examinez ce composant Form :

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        PrĂ©nom :
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Nom :
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Bonjour, {firstName} {lastName}.</b></p>
    </>
  );
}

Il y a de la logique rĂ©pĂ©tĂ©e pour chaque champ du formulaire :

  1. Il y a un Ă©lĂ©ment d’état (firstName et lastName).
  2. Il y a un gestionnaire de changement (handleFirstNameChange et handleLastNameChange).
  3. Il y a un bout de JSX qui spécifie les attributs value et onChange pour ce champ.

Vous pouvez extraire la logique dupliquĂ©e dans ce Hook personnalisĂ© useFormInput :

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

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

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Notez qu’il ne dĂ©clare qu’une seule variable d’état appelĂ©e value.

Pourtant, le composant Form appelle useFormInput deux fois :

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

C’est pourquoi ça revient Ă  dĂ©clarer deux variables d’état distinctes !

Les Hooks personnalisĂ©s vous permettent de partager la logique d’état et non l’état lui-mĂȘme. Chaque appel Ă  un Hook est complĂštement indĂ©pendant de tous les autres appels au mĂȘme Hook. C’est pourquoi les deux bacs Ă  sable ci-dessus sont totalement Ă©quivalents. Si vous le souhaitez, revenez en arriĂšre et comparez-les. Le comportement avant et aprĂšs l’extraction d’un Hook personnalisĂ© est identique.

Lorsque vous avez besoin de partager l’état lui-mĂȘme entre plusieurs composants, faites-le plutĂŽt remonter puis transmettez-le.

Transmettre des valeurs réactives entre les Hooks

Le code contenu dans vos Hooks personnalisĂ©s sera rĂ©exĂ©cutĂ© Ă  chaque nouveau rendu de votre composant. C’est pourquoi, comme les composants, les Hooks personnalisĂ©s doivent ĂȘtre purs. ConsidĂ©rez le code des Hooks personnalisĂ©s comme une partie du corps de votre composant !

Comme les Hooks personnalisĂ©s sont mis Ă  jour au sein du rendu de votre composant, ils reçoivent toujours les props et l’état les plus rĂ©cents. Pour comprendre ce que ça signifie, prenez cet exemple de salon de discussion. Changez l’URL du serveur ou le salon de discussion :

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('Nouveau message : ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        URL du serveur :
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bievenue dans le salon {roomId} !</h1>
    </>
  );
}

Quand vous changez serverUrl ou roomId, l’effet « rĂ©agit Â» Ă  vos changements et se re-synchronise. Vous pouvez voir dans les messages de la console que le chat se reconnecte Ă  chaque fois que vous changez les dĂ©pendances de votre effet.

Maintenant, dĂ©placez le code de l’effet dans un Hook personnalisĂ© :

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Nouveau message : ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Ça permet Ă  votre composant ChatRoom d’appeler le Hook personnalisĂ© sans se prĂ©occuper de la façon dont il fonctionne en interne.

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
URL du serveur :
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Bienvenue dans le salon {roomId} !</h1>
</>
);
}

C’est plus simple ainsi ! (Mais ça fait toujours la mĂȘme chose.)

Remarquez que la logique rĂ©agit toujours aux changement des props et de l’état. Essayez de modifier l’URL du serveur ou le salon choisi :

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        URL du serveur :
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bienvenue dans le salon {roomId} !</h1>
    </>
  );
}

Voyez comme vous rĂ©cupĂ©rez la valeur retournĂ©e par un Hook :

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

puis la transmettez Ă  un autre Hook :

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

Chaque fois que votre composant ChatRoom refait un rendu, il passe les derniĂšres valeurs de roomId et serverUrl Ă  votre Hook. Ça explique pourquoi votre effet se reconnecte au salon Ă  chaque fois que leurs valeurs sont diffĂ©rentes aprĂšs un nouveau rendu. (Si vous avez dĂ©jĂ  travaillĂ© avec des logiciels de traitement d’audio ou de vidĂ©o, ce type d’enchaĂźnement de Hooks peut vous rappeler l’enchaĂźnement d’effets visuels ou sonores. C’est comme si le rĂ©sultat en sortie de useState Ă©tait « branchĂ© » en entrĂ©e de useChatRoom.)

Transmettre des gestionnaires d’évĂ©nements Ă  des Hooks personnalisĂ©s

En construction

Cette section dĂ©crit une API expĂ©rimentale qui n’a pas encore Ă©tĂ© livrĂ©e dans une version stable de React.

Lorsque vous commencerez Ă  utiliser useChatRoom dans davantage de composants, vous souhaiterez peut-ĂȘtre que ces derniers puissent personnaliser son comportement. Par exemple, pour l’instant la logique de traitement quand un message arrive est codĂ©e en dur Ă  l’intĂ©rieur du Hook :

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Nouveau message : ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Disons que vous voulez ramener cette logique dans votre composant :

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('Nouveau message : ' + msg);
}
});
// ...

Pour que ça fonctionne, modifiez votre Hook personnalisĂ© afin qu’il prenne onReceiveMessage comme l’une de ses options :

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ Toutes les dĂ©pendances sont dĂ©clarĂ©es.
}

Ça fonctionnera, mais il y a une autre amĂ©lioration que vous pouvez apporter quand votre Hook personnalisĂ© accepte des gestionnaires d’évĂ©nements.

Ajouter une dĂ©pendance envers onReceiveMessage n’est pas idĂ©al car elle entraĂźnera une reconnexion au salon Ă  chaque rĂ©affichage du composant. Enrobez ce gestionnaire d’état dans un ÉvĂ©nement d’Effet pour le retirer des dĂ©pendances :

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Toutes les dĂ©pendances sont dĂ©clarĂ©es.
}

À prĂ©sent, le salon ne se reconnectera plus Ă  chaque rĂ©affichage du composant ChatRoom. Voici une dĂ©monstration complĂšte du passage d’un gestionnaire d’évĂ©nement Ă  un Hook personnalisĂ© avec laquelle vous pouvez jouer :

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('Nouveau message: ' + msg);
    }
  });

  return (
    <>
      <label>
        URL du serveur :
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Bienvenue dans le salon {roomId} !</h1>
    </>
  );
}

Remarquez que vous n’avez plus besoin de savoir comment useChatRoom fonctionne pour pouvoir l’utiliser. Vous pourriez l’ajouter Ă  n’importe quel autre composant, lui passer n’importe quelles autres options, il fonctionnerait de la mĂȘme maniĂšre. C’est lĂ  toute la puissance des Hooks personnalisĂ©s.

Quand utiliser des Hooks personnalisĂ©s ?

Il n’est pas nĂ©cessaire d’extraire un Hook personnalisĂ© pour chaque petit bout de code dupliquĂ©. Certaines duplications sont acceptables. Par exemple, extraire un Hook useFormInput pour enrober un seul appel de useState comme prĂ©cĂ©demment est probablement inutile.

Cependant, Ă  chaque fois que vous Ă©crivez un Effet, demandez-vous s’il ne serait pas plus clair de l’enrober Ă©galement dans un Hook personnalisĂ©. Vous ne devriez pas avoir si souvent besoin d’Effets, alors si vous en Ă©crivez un, ça signifie que vous devez « sortir Â» de React pour vous synchroniser avec un systĂšme extĂ©rieur ou pour faire une chose pour laquelle React n’a pas d’API intĂ©grĂ©e. L’enrober dans un Hook personnalisĂ© permet de communiquer prĂ©cisĂ©ment votre intention et la maniĂšre dont les flux de donnĂ©es circulent Ă  travers lui.

Prenons l’exemple d’un composant ShippingForm qui affiche deux listes dĂ©roulantes : l’une prĂ©sente la liste des villes, l’autre affiche la liste des quartiers de la ville choisie. Vous pourriez dĂ©marrer avec un code ressemblant Ă  ceci :

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Cet effet rĂ©cupĂšre les villes d’un pays.
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Cet effet récupÚre les quartiers de la ville choisie.
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

Bien que ce code soit assez rĂ©pĂ©titif, il est lĂ©gitime de garder ces effets sĂ©parĂ©s les uns des autres. Ils synchronisent deux choses diffĂ©rentes, vous ne devez donc pas les fusionner en un seul Effet. Vous pouvez plutĂŽt simplifier le composant ShippingForm ci-dessus en sortant la logique commune entre eux dans votre propre Hook useData :

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

Vous pouvez maintenant remplacer les deux Effets du composant ShippingForm par des appels Ă  useData :

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

Extraire un Hook personnalisĂ© rend le flux des donnĂ©es explicite. Vous renseignez l’url, et vous obtenez les data en retour. En « masquant Â» votre Effet dans useData, vous empĂȘchez Ă©galement qu’une autre personne travaillant sur le composant ShippingForm y ajoute des dĂ©pendances inutiles. Avec le temps, la plupart des Effets de votre appli se trouveront dans des Hooks personnalisĂ©s.

En détail

RĂ©servez vos Hooks personnalisĂ©s Ă  des cas d’usage concrets de haut niveau

Commencez par choisir le nom de votre Hook personnalisĂ©. Si vous avez du mal Ă  choisir un nom clair, ça peut signifier que votre effet est trop liĂ© au reste de la logique de votre composant, et qu’il n’est pas encore prĂȘt Ă  ĂȘtre extrait.

Dans l’idĂ©al, le nom de votre Hook personnalisĂ© doit ĂȘtre suffisamment clair pour qu’une personne qui n’écrit pas souvent du code puisse deviner ce que fait votre Hook, ce qu’il prend en arguments et ce qu’il renvoie :

  • ✅ useData(url)
  • ✅ useImpressionLog(eventName, extraData)
  • ✅ useChatRoom(options)

Lorsque vous vous synchronisez avec un systĂšme extĂ©rieur, le nom de votre Hook personnalisĂ© peut ĂȘtre plus technique et utiliser un jargon spĂ©cifique Ă  ce systĂšme. C’est une bonne chose tant que ça reste clair pour une personne qui a l’habitude de ce systĂšme :

  • ✅ useMediaQuery(query)
  • ✅ useSocket(url)
  • ✅ useIntersectionObserver(ref, options)

Les Hooks personnalisĂ©s doivent ĂȘtre concentrĂ©s sur des cas d’usage concrets de haut niveau. Évitez de recourir Ă  des Hooks personnalisĂ©s de « cycle de vie Â» qui agissent comme des alternatives et des enrobages de confort pour l’API useEffect elle-mĂȘme :

  • 🔮 useMount(fn)
  • 🔮 useEffectOnce(fn)
  • 🔮 useUpdateEffect(fn)

Par exemple, ce Hook useMount essaie de s’assurer que du code ne s’exĂ©cute qu’au « montage Â» :

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔮 À Ă©viter : utiliser des Hooks personnalisĂ©s de « cycle de vie Â».
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔮 À Ă©viter : crĂ©er des Hooks personnalisĂ©s de « cycle de vie Â».
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔮 Le Hook React useEffect a une dĂ©pendance manquante : 'fn'
}

Les Hooks personnalisĂ©s de « cycle de vie Â» comme useMount ne s’intĂšgrent pas bien dans le paradigme de React. Par exemple, ce code contient une erreur (il ne « rĂ©agit Â» pas aux changements de roomId ou serverUrl), mais le linter ne vous avertira pas Ă  ce sujet car il ne vĂ©rifie que les appels directs Ă  useEffect. Il ne connaĂźt rien de votre Hook.

Si vous Ă©crivez un effet, commencez par utiliser directement l’API de React :

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Correct : deux effets bruts sĂ©parĂ©s par leur finalitĂ©.

useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

Ensuite, vous pouvez (mais ce n’est pas obligatoire) extraire des Hooks personnalisĂ©s pour diffĂ©rents cas d’usage de haut niveau :

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Excellent : des Hooks personnalisĂ©s nommĂ©s selon leur fonction.
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Un bon Hook personnalisĂ© rend le code appelĂ© plus dĂ©claratif en limitant ce qu’il fait. Par exemple, useChatRoom(options) ne peut que se connecter Ă  un salon de discussion, tandis que useImpressionLog(eventName, extraData) ne peut que contribuer aux journaux analytiques. Si l’API de votre Hook personnalisĂ© ne limite pas les cas d’usage en Ă©tant trop abstraite, elle risque d’introduire Ă  long terme plus de problĂšmes qu’elle n’en rĂ©soudra.

Les Hooks personnalisés vous aident à migrer vers de meilleures approches

Les Effets sont une « Ă©chappatoire Â» : vous les utilisez quand vous avez besoin de « sortir Â» de React et quand il n’y a pas de meilleure solution intĂ©grĂ©e pour votre cas d’usage. Avec le temps, le but de l’équipe de React est de rĂ©duire au maximum le nombre d’Effets dans votre appli en fournissant des solutions plus dĂ©diĂ©es Ă  des problĂšmes plus spĂ©cifiques. Enrober vos Effets dans des Hooks personnalisĂ©s facilite la mise Ă  jour de votre code lorsque ces solutions deviendront disponibles.

Revenons Ă  cet exemple :

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

Dans l’exemple ci-dessus, useOnlineStatus est implĂ©mentĂ© avec le duo useState et useEffect. Cependant, ce n’est pas la meilleure solution possible. Elle ne tient pas compte d’un certain nombre de cas limites. Par exemple, elle suppose que lorsque le composant est montĂ©, isOnline est dĂ©jĂ  Ă  true, mais ça peut ĂȘtre faux si le rĂ©seau est d’entrĂ©e de jeu hors-ligne. Vous pouvez utiliser l’API du navigateur navigator.onLine pour vĂ©rifier ça, mais l’utiliser directement ne marchera pas sur le serveur pour gĂ©nĂ©rer le HTML initial. En bref, ce code peut ĂȘtre amĂ©liorĂ©.

Heureusement, React 18 inclut une API dĂ©diĂ©e appelĂ©e useSyncExternalStore qui se charge de tous ces problĂšmes pour vous. Voici votre Hook personnalisĂ© useOnlineStatus réécrit pour en tirer avantage :

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // Comment récupérer la valeur sur le client.
    () => true // Comment récupérer la valeur sur le serveur.
  );
}

Remarquez que vous n’avez pas eu besoin de modifier les composants pour faire cette migration :

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

C’est une raison pour laquelle il est souvent utile d’enrober des effets dans des Hooks personnalisĂ©s :

  1. Vous rendez le flux de données vers et depuis vos effets trÚs explicite.
  2. Vous permettez Ă  vos composants de se concentrer sur l’intention plutĂŽt que sur l’implĂ©mentation exacte de vos effets.
  3. Lorsque React ajoute de nouvelles fonctionnalitĂ©s, vous pouvez retirer ces effets sans changer le code d’aucun de vos composants.

À la maniĂšre d’un Design System, vous pourriez trouver utile de commencer Ă  extraire les idiomes communs des composants de votre appli dans des Hooks personnalisĂ©s. Ainsi, le code de vos composants restera centrĂ© sur l’intention et vous Ă©viterez la plupart du temps d’utiliser des effets bruts. De nombreux Hooks personnalisĂ©s de qualitĂ© sont maintenus par la communautĂ© React.

En détail

React fournira-t-il une solution intĂ©grĂ©e pour le chargement de donnĂ©es ?

Nous travaillons encore sur les dĂ©tails, mais nous pensons qu’à l’avenir, vous pourrez charger des donnĂ©es comme ceci :

import { use } from 'react'; // Pas encore disponible !

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

Si vous utilisez des Hooks personnalisĂ©s comme le useData vu plus haut dans votre appli, la migration vers l’approche finalement recommandĂ©e nĂ©cessitera moins de changements que si vous Ă©crivez manuellement des effets bruts dans chaque composant. Cependant, l’ancienne approche continuera de bien fonctionner, donc si vous vous sentez Ă  l’aise en Ă©crivant des effets bruts, vous pouvez continuer ainsi.

Il y a plus d’une façon de faire

Supposons que vous souhaitiez implĂ©menter une animation de fondu enchaĂźnĂ© en partant de zĂ©ro avec l’API requestAnimationFrame du navigateur. Vous pouvez commencer par un Effet qui initialise une boucle d’animation. À chaque Ă©tape de l’animation, vous pourriez changer l’opacitĂ© du nƓud du DOM que vous aurez conservĂ© dans une ref jusqu’à ce qu’elle atteigne 1. Votre code pourrait commencer ainsi :

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

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // Nous avons encore des étapes à dessiner.
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Bienvenue
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Supprimer' : 'Afficher'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Pour rendre le composant plus lisible, vous pourriez extraire la logique dans un Hook personnalisĂ© useFadeIn :

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Bienvenue
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Supprimer' : 'Afficher'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Vous pouvez conserver le code de useFadeIn tel quel, mais vous pouvez le remanier plus avant. Par exemple, vous pourriez extraire la logique de mise en place de la boucle d’animation de useFadeIn dans un Hook personnalisĂ© useAnimationLoop :

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

Cependant, vous n’avez pas besoin de faire ça. Comme pour les fonctions ordinaires, c’est finalement Ă  vous de dĂ©finir les frontiĂšres entre les diffĂ©rentes parties de votre code. Vous pouvez Ă©galement adopter une approche tout Ă  fait diffĂ©rente. Au lieu de conserver votre logique dans un effet, vous pouvez dĂ©placer la plupart de la logique impĂ©rative dans une classe JavaScript :

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

Les Effets permettent Ă  React de se connecter Ă  des systĂšmes extĂ©rieurs. Plus la coordination entre les Effets est nĂ©cessaire (par exemple pour enchaĂźner des animations multiples), plus il devient pertinent de sortir complĂštement cette logique des Effets et des Hooks, comme dans le bac Ă  sable ci-dessus. Le code extrait devient alors le « systĂšme extĂ©rieur Â». Ça permet Ă  vos Effets de rester simples car ils n’auront qu’à envoyer des messages au systĂšme que vous avez sorti de React.

Les exemples ci-dessus supposent que la logique de fondu enchaĂźnĂ© soit Ă©crite en JavaScript. Cependant, cette animation particuliĂšre est Ă  la fois plus simple et beaucoup plus efficace lorsqu’elle est implĂ©mentĂ©e comme une simple animation CSS :

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Parfois, vous n’avez mĂȘme pas besoin d’un Hook !

En résumé

  • Les Hooks personnalisĂ©s vous permettent de partager la logique entre les composants.
  • Le nom des Hooks personnalisĂ©s doit commencer par use suivi d’une majuscule.
  • Les Hooks personnalisĂ©s ne partagent que la logique d’état et non l’état lui-mĂȘme.
  • Vous pouvez passer des valeurs rĂ©actives d’un Hook Ă  un autre, et elles restent Ă  jour.
  • Tous les Hooks sont rĂ©exĂ©cutĂ©s Ă  chaque rendu de votre composant.
  • Le code de vos Hooks personnalisĂ©s doit ĂȘtre pur, comme le code de votre composant.
  • Enrobez les gestionnaires d’évĂ©nements reçus par les Hooks personnalisĂ©s dans des ÉvĂ©nĂ©ments d’Effet.
  • Ne crĂ©ez pas des Hooks personnalisĂ©s comme useMount. Veillez Ă  ce que leur objectif soit spĂ©cifique.
  • C’est Ă  vous de dĂ©cider comment et oĂč dĂ©finir les frontiĂšres de votre code.

DĂ©fi 1 sur 5 Â·
Extraire un Hook useCounter

Ce composant utilise une variable d’état et un effet pour afficher un nombre qui s’incrĂ©mente Ă  chaque seconde. Extrayez cette logique dans un Hook personnalisĂ© appelĂ© useCounter. Votre but est de faire que l’implĂ©mentation du composant Counter ressemble exactement Ă  ça :

export default function Counter() {
const count = useCounter();
return <h1>Secondes Ă©coulĂ©es : {count}</h1>;
}

Vous devrez Ă©crire votre Hook personnalisĂ© dans useCounter.js et l’importer dans le fichier App.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Secondes Ă©coulĂ©es : {count}</h1>;
}