Dernièrement, j’ai vu beaucoup de discussions contradictoires dans les forums React concernant les composants de classe et les hooks. Pour les développeurs React juniors et plus avancés, je voudrais clarifier en répondant à certaines des questions les plus fréquemment posées concernant les crochets par rapport aux composants de classe.
TL;DR Arrête d’utiliser les composants de classe. Les composants fonctionnels avec crochets sont le meilleur choix et ce depuis des années.
Quelle est la différence entre React et React Hooks ?
React est une bibliothèque qui nous aide à créer des interfaces utilisateur interactives dans le navigateur. Les crochets sont un outil de React qui fournit des moyens de gérer l’état et de réagir aux changements au sein de notre application. React Hooks n’est pas une bibliothèque spéciale, ce n’est qu’un autre outil intégré à React depuis la version 16.8.
Les crochets React remplacent-ils les composants de classe ?
Oui. Eh bien, en quelque sorte?
Il est un peu plus précis de dire que les composants de fonction avec des crochets remplacent les composants de classe. Les crochets ne fonctionnent que dans les composants de fonction, donc si vous voulez utiliser des crochets, vous ne pouvez pas utiliser de composants de classe. Outre les limites d’erreur, un composant de fonction avec des crochets peut accomplir tout ce que les composants de classe peuvent faire avec un code plus lisible et succinct.
Exemple de classe (TypeScript)
Tout d’abord, je vais aborder le composant de classe. Bien qu’assez basique, cet exemple présente la récupération de données, un accessoire lié à la récupération de données et la nécessité de récupérer les données lorsqu’un accessoire change.
import React from "react";
import { StuffType } from "../../types/stuff";
import { fetchStuffList } from "../../utils/fetchStuffList";
type Props = {
stuffQuery: string
}
type State = {
stuff: StuffType[];
loading: boolean;
}
export class ListStuffClass extends React.Component<Props, State> {
state: State = {
stuff: [],
loading: false,
};
componentDidMount() {
this.loadStuff()
}
componentDidUpdate(prevProps: Props) {
if (prevProps.stuffQuery !== this.props.stuffQuery) {
this.loadStuff()
}
}
loadStuff = () => {
this.setState({ loading: true })
fetchStuffList(this.props.stuffQuery)
.then(data => {
this.setState({ loading: false, stuff: data })
})
}
render() {
if (this.state.loading) return (<div>Loading...</div>);
return (
<div>
{this.state.stuff.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</div>
);
}
}
Exemple de fonction (TypeScript)
Dans cet exemple de composant de fonction avec un crochet personnalisé, la longueur de code combinée est un peu plus courte qu’avec le composant de classe, mais il y a des avantages plus importants.
D’une part, nous créons naturellement un crochet personnalisé qui sépare notre logique de récupération et peut être réutilisé ailleurs. Notez également que le composant de classe a besoin de 2 ensembles de logique pour savoir quand récupérer les données, alors que le hook en a un. Les données seront récupérées chaque fois que la valeur de la requête est nouvelle, y compris lors du premier montage.
import { useEffect, useState } from "react"
import type { StuffType } from "../../types/stuff"
import { fetchStuffList } from "../../utils/fetchStuffList"
export const useStuffList = (stuffQuery: string) => {
const [loading, setLoading] = useState<boolean>(false)
const [stuff, setStuff] = useState<StuffType[]>([])
const loadStuff = (stuffQuery: string) => {
setLoading(true)
fetchStuffList(stuffQuery)
.then(data => {
setLoading(false)
setStuff(data)
})
}
useEffect(() => {
loadStuff(stuffQuery)
}, [stuffQuery])
return { loading, stuff }
}
Et maintenant pour le composant de fonction. En déplaçant la récupération vers un crochet personnalisé, vous pouvez voir que ce fichier devient beaucoup plus simple que le composant de classe ci-dessus.
import { useStuffList } from "./useStuffList";
type Props = {
stuffQuery: string;
};
export function ListStuffFunction({ stuffQuery }: Props) {
const { loading, stuff } = useStuffList(stuffQuery);
if (loading) return <div>Loading...</div>;
return (
<div>
{stuff.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</div>
);
}
Pensez-vous que l’avenir de ReactJS est React Hooks ou Class Components ?
Crochets.
Au départ, les composants de classe étaient répandus car ils pouvaient suivre l’état et répondre à des événements spécifiques du cycle de vie des composants. Des composants fonctionnels existaient ; cependant, ils étaient limités car ils ne pouvaient pas accéder à l’État. Les composants fonctionnels étaient communément appelés apatride composants jusqu’à la sortie de l’API React Hooks dans v16
.
Pour beaucoup d’entre nous utilisant React de manière professionnelle avant l’introduction des crochets, il y avait beaucoup de frustration avec les composants de classe. La réutilisation de la logique des données a entraîné des modèles désordonnés tels que les composants d’ordre supérieur (HOC) ou des hacks tels que Render Props. Celles-ci étaient douloureuses dans les grandes applications.
Lorsque l’équipe React a introduit les crochets, il n’a pas fallu longtemps pour réaliser la puissance et la flexibilité des crochets. Les composants fonctionnels avec crochets ont permis d’obtenir un code plus propre et plus lisible. L’entreprise dans laquelle je travaillais a commencé à remplacer les composants de classe en quelques mois, et je savais que nous n’étions pas seuls. Je ne peux pas penser à un autre changement majeur dans un cadre, une bibliothèque ou un langage qui a été adopté si rapidement par sa communauté.
Dois-je apprendre React Hooks ou des composants basés sur des classes ?
Vous devez absolument vous concentrer sur l’apprentissage des composants fonctionnels avec des crochets. La majorité du monde professionnel de React est passé à l’utilisation de crochets peu de temps après leur introduction dans React v16.8. C’était au début de 2019. À l’exception des limites d’erreur, je n’ai pas écrit de composant de classe depuis plus de 3 ans.
Si React Hooks est si génial et que les composants de classe sont obsolètes, pourquoi la documentation enseigne-t-elle les composants de classe ?
Depuis février 2023, les documents officiels de React sont cruellement obsolètes. L’équipe React a travaillé sur une nouvelle version de leurs docs qui devrait sortir prochainement.
Si les docs React que vous consultez utilisent encore des composants de classe, nous vous conseillons vivement d’utiliser les docs bêta React, où toutes les explications sont écrit à l’aide de crochets plutôt que des cours.
Bien que les nouvelles documentations soient peut-être en ligne au moment où vous lisez ceci, beaucoup d’entre vous ont commencé à apprendre alors que les anciennes documentations sur les composants de classe étaient encore d’actualité. Si vous utilisez actuellement des composants de classe, je vous invite à lire la nouvelle documentation et à vous familiariser avec la manière actuelle de faire les choses. Il y aura un peu de courbe d’apprentissage lorsque vous désapprendreez les méthodes de cycle de vie, les accessoires de rendu et les composants d’ordre supérieur (HOC), mais le temps en vaudra la peine.
Les crochets React ont-ils rendu les composants de classe obsolètes ?
Absolument, à une exception près : les limites d’erreur doivent toujours être des composants de classe.
Pour créer un composant de classe, vous devez étendre React.Component
. Dans la nouvelle version de la documentation React, Component
est répertorié dans la section Legacy et renvoie à un didacticiel sur le passage aux composants de fonction.
À partir de la page Legacy de la nouvelle documentation :
Ces API sont exportées depuis le
react
package, mais ils ne sont pas recommandés pour une utilisation dans le code nouvellement écrit.
Quels sont les hooks les plus importants dans React ?
Les crochets dont vous aurez besoin sont spécifiques à l’application, mais vous trouverez ci-dessous quelques-uns des plus courants et des plus populaires. J’ai inclus des exemples concrets de crochets React utiles que vous pouvez utiliser dans vos projets.
ID d’utilisation
L’équipe React a récemment ajouté le ID d’utilisation crochet pour fournir des identifiants stables et uniques pour les entrées de formulaire accessibles. Les étiquettes doivent être liées aux entrées du formulaire. La façon la plus courante de le faire est d’utiliser des identifiants uniques. Étant donné que les projets React encouragent la réutilisation des composants, cela peut être difficile lorsqu’un formulaire a plusieurs entrées du même type. Vous ne pouvez pas simplement utiliser id="text-field"
ou même id="first-name"
car ce même ID peut être réutilisé sur la même page.
Grace à ID d’utilisation hook, vous pouvez créer une étiquette personnalisée + un composant d’entrée avec un ID stable et unique pour lier les deux parties ensemble.
import { useId } from "react";
export function TextInput(props: {
label: string;
value: string;
onChange: (value: string) => void;
}) {
const id = useId();
return (
<div>
<label htmlFor={id}>{props.label}</label>
<input
id={id}
type="text"
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
/>
</div>
);
}
useState
C’est le crochet le plus couramment utilisé dans la plupart des bases de code. Il fournit une API simple pour stocker l’état, par exemple :
import { Link } from "react-router-dom";
import { useState } from "react";
type MenuOption = {
label: string;
to: string;
}
export function Menu({ menuOptions }: { menuOptions: MenuOption[]}) {
const [menuOpen, setMenuOpen] = useState(false);
return (
<div>
<button onClick={(prevValue) => setMenuOpen(!prevValue)}>
Menu Toggle
</button>
{menuOpen && (
<div>
{menuOptions.map((option) => (
<Link key={option.label} to={option.to}>{option.label}</Link>
))}
</div>
)}
</div>
)
}
utiliserEffet
Lorsque quelque chose change dans votre application, useEffect fournit une interface pour répondre à ce changement.
-
Le composant est monté, répondez en récupérant les données requises sur le serveur
-
La valeur du formulaire est sélectionnée et est en conflit avec une autre valeur, répondez en passant à une autre valeur
-
L’emplacement a changé, répondez en fermant le menu de navigation
import { Link, useLocation } from "react-router-dom";
import { useEffect, useState } from "react";
type MenuOption = {
label: string;
to: string;
}
export function Menu({ menuOptions }: { menuOptions: MenuOption[] }) {
// custom hook imported from React Router that watches the URL
const location = useLocation();
const [menuOpen, setMenuOpen] = useState(false);
// close the menu when the URL changes
useEffect(() => setMenuOpen(false), [location]);
return (
<div>
<button onClick={(prevValue) => setMenuOpen(!prevValue)}>
Menu Toggle
</button>
{menuOpen && (
<div>
{menuOptions.map((option) => (
<Link key={option.label} to={option.to}>{option.label}</Link>
))}
</div>
)}
</div>
)
}
Le crochet useEffect est essentiel mais aussi le plus surutilisé et le plus sujet aux bogues de tous les crochets. Pour éviter les problèmes avec useEffect, voici quelques conseils :
Utilisez eslint et suivez les règles des crochets
Installation eslint
et eslint-plugin-react
vous obtiendrez des avertissements dans votre éditeur pour les dépendances manquées et les dépendances qui doivent être enveloppées useCallback
ou useMemo
.
Méthodes de cycle de vie !== useEffect
Sachez que useEffect peut être utilisé de la même manière que les méthodes de cycle de vie des composants de classe, mais ce n’est pas la même chose.
Soyez prudent lorsque vous appelez la fonction setState de useState dans un useEffect.
Sachez que setState déclenchera un autre rendu, qui peut ensuite déclencher un autre useEffect, useMemo ou useCallback, qui peut se transformer en plusieurs rendus coûteux ou même une boucle infinie. Si la valeur d’état définie est une dépendance de ce même useEffect, cela provoquera absolument une boucle infinie.
Considérez si l’action pourrait plutôt être placée dans le gestionnaire d’événements qui a provoqué le changement.
Une bonne règle empirique consiste à vous demander si l’action doit se produire en tant qu’effet secondaire d’un changement de valeur d’état ( utiliserEffet ) ou si l’action est une conséquence directe de l’interaction de l’utilisateur avec l’interface utilisateur ( gestionnaire d’événements ).
Dans l’exemple de menu ci-dessus, vous pouvez à la place ajouter un onClick
au lien, qui s’exécutera avant la navigation. Dans certains cas, éviter useEffect est préférable et plus prévisible, bien que dans ce cas, vous pourriez avoir d’autres liens dans d’autres parties de la page dont le composant de menu n’est pas conscient, useEffect est donc le meilleur choix.
<Link to={option.to} onClick={() => setMenuOpen(false)}>
{option.label}
</Link>
Vous n’avez pas besoin de useEffect pour les transformations de données.
Un exemple de ceci est une table de données avec des options de filtre. Lorsque l’utilisateur modifie les filtres, il est courant de voir les développeurs définir le nouvel état du filtre et faire en sorte qu’un useEffect détecte le changement de filtre, qui filtre ensuite à nouveau les données de la table et exécute un autre état défini. Le problème est que ce composant s’exécutera deux fois : une fois pour la nouvelle valeur de filtre et une autre fois pour les données de table nouvellement filtrées. Nous voulons éviter ces re-rendus inutiles et coûteux.
Au lieu de cela, exécutez votre transformation de données en haut du composant à chaque fois. Si vous décomposez correctement vos composants (pensez au principe de responsabilité unique), alors généralement, tout changement dans l’état du composant entraînera de toute façon un recalcul des filtres de données.
Commençons par la bonne façon de gérer une transformation de données :
// The right way
type PeopleTableProps = { people: Array<{ name: string, age: number }> }
export function PeopleTable({ people }: PeopleTableProps) {
const [nameQuery, setNameQuery] = useState<string>('')
const filteredPeople = people.filter(person => person.name.includes(nameQuery))
return (
<div>
<label htmlFor="name-query">Name</label>
<input id="name-query" type="text" onChange={e => setNameQuery(e.target.value)} />
<table>
<tbody>
{filteredPeople.map(person => (
<tr key={person.name}>
<td>{person.name}</td>
<td>{person.age}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
L’approche suivante est incorrecte car elle déclenchera 2 rendus pour chaque modification de la valeur du filtre.
// The wrong way
export function PeopleTable({ people }: PeopleTableProps) {
const [nameQuery, setNameQuery] = useState<string>('')
const [filteredPeople, setFilteredPeople] = useState<Array<{ name: string, age: number }>>(people)
useEffect(() => {
setFilteredPeople(people.filter(person => person.name.includes(nameQuery)))
}, [nameQuery, people])
return (
<div>
<label htmlFor="name-query">Name</label>
<input id="name-query" type="text" onChange={e => setNameQuery(e.target.value)} />
...
</div>
)
}
Si vous avez encore des valeurs d’état ou de prop déclenchant un rendu, mais qu’elles ne nécessitent pas que les données soient refiltrées, recherchez useMemo.
utiliserMémo
Lorsque les valeurs changent dans un composant, React restituera ledit composant. De manière générale, c’est exactement ce que nous voulons, mais il y a des exceptions. Un exemple particulièrement courant est une liste de données qui doivent être triées ou filtrées.
Lorsque l’ordre de tri ou les valeurs de filtre changent, nous voulons que notre composant obtienne un nouveau tableau de données et restitue avec ce nouvel ensemble de données. Tout bon.
En reprenant l’exemple précédent, imaginons que nous voulions trier notre liste de choses et que le serveur ne puisse pas le faire pour nous. Si nous avions affaire à des milliers d’éléments dans notre tableau, le tri de ces données serait sensiblement coûteux. Sans useMemo, la valeur sortedStuff serait recalculée dès que stuffQuery changerait, puis à nouveau lorsque la récupération serait terminée et définirait une nouvelle valeur stuff. En utilisant le crochet useMemo, nous pouvons limiter et contrôler le moment où la valeur est reconstruite.
export const useStuffList = (stuffQuery: string) => {
const [loading, setLoading] = useState<boolean>(false);
const [stuff, setStuff] = useState<StuffType[]>([]);
const loadStuff = (stuffQuery: string) => {
setLoading(true);
fetchStuffList(stuffQuery).then((data) => {
setLoading(false);
setStuff(data);
});
};
useEffect(() => {
loadStuff(stuffQuery);
}, [stuffQuery]);
const sortedStuff = useMemo(() => {
return stuff.sort((a, b) =>
a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? -1 : 1
);
}, [stuff]);
return { loading, stuff, sortedStuff };
};
L’une des principales utilisations de useMemo est de gérer la stabilité d’un useEffect qui dépend d’une valeur, en particulier d’objets et de tableaux qui ne sont pas contenus dans un useState. Étant donné que useEffect s’exécute à nouveau chaque fois que la référence de la valeur change, useMemo garantit que useEffect ne s’exécute pas trop souvent en s’assurant que la valeur n’est pas recréée trop souvent.
Notez que useMemo peut également être utilisé comme amélioration des performances, et une optimisation prématurée est une mauvaise idée. N’utilisez pas useMemo simplement parce que vous le pouvez. Utilisez-le plutôt comme une réponse aux problèmes de performances. L’ajout de useMemo a un coût de performance en soi, donc une utilisation excessive peut nuire aux performances.
utiliserCallback
Là où useMemo est un emplacement stable pour conserver les valeurs calculées, useCallback est un emplacement stable pour conserver les fonctions qui utilisent des valeurs d’état et de prop. Comme avec useMemo, un useCallback stabilise un useEffect lorsqu’une fonction dérivée est une dépendance.
La recréation de fonctions a un coût et peut entraîner des rendus descendants, donc useCallback peut également corriger les problèmes de performances, mais comme useMemo, useCallback a un coût de performances en soi. La surutilisation peut nuire, plutôt que d’aider, la performance.
useReducer
En utilisant un modèle de réducteur similaire à Redux, useReducer peut être utilisé pour gérer des états plus complexes, en particulier lorsque les valeurs d’état interagissent les unes avec les autres. Des actions sont définies qui peuvent modifier simultanément plusieurs valeurs dans un état complexe. Un exemple est un outil de sélection de véhicule avec une liste déroulante pour la marque, le modèle et le niveau de finition. Lorsque l’utilisateur modifie la valeur make, toutes les autres valeurs doivent disparaître et les options de ces listes déroulantes doivent changer.
Bien que vous puissiez orchestrer tous ces changements d’état avec plusieurs crochets useState, les crochets useReducer vous donneront un code plus simple tout en garantissant que tous les changements se produisent exactement au même moment.
Recherchez useState en premier et la plupart du temps, mais sachez que useReducer existe pour vos composants les plus complexes. Lorsque vous arrivez au point d’atteindre useReducer, sachez qu’il s’agit d’une interface complexe avec laquelle travailler, et vous feriez peut-être mieux d’utiliser une bibliothèque d’état comme Redux Toolkit. Envisagez également de refactoriser votre composant au lieu d’utiliser useReducer. Diviser votre composant en plusieurs composants peut parfois résoudre le problème de complexité sans avoir besoin d’une solution d’état plus complexe.
Je ne vais pas donner d’exemple de useReducer car il s’agit d’un crochet plus avancé que celui qui devrait être couvert ici. Je suggère de creuser dans les documents React si vous pensez en avoir besoin.
Conclusion
J’espère avoir répondu à certaines de vos principales questions sur React Hook. Je pense que les crochets étaient un excellent ajout à React et qu’une bonne compréhension des crochets disponibles est essentielle pour être un excellent développeur React. La prochaine étape consiste à apprendre à créer vos propres crochets personnalisés.
Avez-vous des pensées?
Ne manquez jamais une mise à jour !
Abonnez-vous au blog 📬