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 :
- Un Ă©lĂ©ment dâĂ©tat qui dĂ©termine si le rĂ©seau est en ligne ou non.
- Un effet qui sâabonne aux Ă©vĂ©nements globaux
online
etoffline
, 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 :
- Les noms des composants React doivent commencer par une majuscule, comme
StatusBar
etSaveButton
. Les composants React doivent également renvoyer quelque chose que React sait afficher, comme un bout de JSX. - Les noms des Hooks doivent commencer par
use
suivi dâune majuscule, commeuseState
(fourni) ouuseOnlineStatus
(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.
En détail
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 :
- Il y a un Ă©lĂ©ment dâĂ©tat (
firstName
etlastName
). - Il y a un gestionnaire de changement (
handleFirstNameChange
ethandleLastNameChange
). - Il y a un bout de JSX qui spécifie les attributs
value
etonChange
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
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
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 :
- Vous rendez le flux de données vers et depuis vos effets trÚs explicite.
- Vous permettez Ă vos composants de se concentrer sur lâintention plutĂŽt que sur lâimplĂ©mentation exacte de vos effets.
- 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
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>; }