Composants de haut niveau, des enveloppes de classe aux fonctions et aux hooks
Apprenez comment fonctionnent les composants de haut niveau de React, à quoi ressemble le même HOC en tant que classe et en tant que fonction, et pourquoi les hooks personnalisés ont remplacé la plupart des cas d’usage des HOC.
Sources: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)
Bien avant l’apparition des hooks personnalisés, les développeurs React avaient besoin d’un moyen de partager des comportements tels que l’enregistrement des événements, les vérifications d’authentification ou le chargement de données entre de nombreux composants. La solution était le composant d’ordre supérieur (HOC) : une fonction simple qui reçoit un composant et en génère un enveloppe améliorée. Ci-dessous, le même HOC d’enregistrement est implémenté sous forme de classe et de fonction, puis comparé à un hook personnalisé.
Pourquoi les HOC sont apparus à l’époque des classes
Lorsque la plupart des codes React étaient écrits sous forme de classes, il était difficile de réutiliser la logique. Imaginez que Profile, Dashboard et Settings aient tous besoin de journaliser leur chargement et leurs mises à jour, de vérifier que l’utilisateur est connecté, de charger des données et de gérer les erreurs. Sans une abstraction partagée, chaque classe répétait le même constructeur, la même configuration d’état, les mêmes méthodes de cycle de vie et les mêmes méthodes d’aide. Les HOC ont résolu ce problème en enveloppant une classe dans une autre classe qui gère la logique de cycle de vie commune.
Un HOC n’est pas une API spéciale de React, c’est simplement une composition de fonctions appliquée aux composants.
Un HOC de journalisation construit avec des classes
Rédaction du wrapper
L’HOC suivant retourne une nouvelle classe qui journalise le chargement du composant enveloppé ainsi que chacune de ses mises à jour, puis affiche le composant original en transmettant tous les props reçus :
import React, { Component } from "react";
export function withRenderLogger(WrappedComponent) {
return class extends Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentDidUpdate() {
console.log(`${WrappedComponent.name} updated`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Le conteneur gère les méthodes de cycle de vie et transmet this.props tel quel, de sorte que la composante originale ne contient aucun code d’affichage.
L’application
Vous exportez la version contournée plutôt que la classe brute :
import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";
class Profile extends Component {
render() {
return <h2>Profile Component</h2>;
}
}
export default withRenderLogger(Profile);
Le rendu de Profile affiche désormais le conteneur, qui à son tour affiche le véritable Profile. La classe retournée est anonyme, donc définir un displayName comme withRenderLogger(Profile) facilite la lecture dans React DevTools.
Les inconvénients des HOC basés sur des classes
Cela fonctionnait, mais avec des difficultés :
- des définitions de classes imbriquées et un liage à
this - une logique répartie dans les méthodes de cycle de vie
- des arbres de composantes plus profonds, ce qui rend le débogage complexe en raison des conteneurs
- des collisions de noms de propriétés lorsque plusieurs HOC injectent des propriétés portant le même nom
Même principe avec les composants fonctionnels
Sans classes, this ni méthodes de cycle de vie, un HOC se réduit à une fonction qui retourne un composant fonctionnel :
function HOC(WrappedComponent) {
return function Enhanced(props) {
return <WrappedComponent {...props} />;
};
}
Voici le générateur de logs réécrit dans ce style :
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
L’utilisation est identique à la version basée sur des classes :
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
Il enregistre des logs à chaque rendu, mais en tant qu’effet secondaire au sein du corps du rendu. React peut rendre plus fréquemment qu’il ne sauvegarde les données, et le mode strict rend deux fois en développement, donc attendez-vous à des logs dupliqués ; utilisez useEffect pour tout ce qui dépasse une simple débogage rapide.
Une petite application de démonstration
Un bouton permet de basculer entre le Profile basé sur des classes et le Dashboard basé sur des fonctions, de sorte que les deux HOC enregistrent les données. Le fait d’utiliser la rendu plutôt que des erreurs pour faire fonctionner la démonstration garantit sa prévisibilité sous Fast Refresh :
import React, { useState } from "react";
import Profile from "./Profile";
import Dashboard from "./Dashboard";
export default function App() {
const [toggle, setToggle] = useState(true);
return (
<div style={{ padding: 20 }}>
<h1>HOC Demo</h1>
<button onClick={() => setToggle(!toggle)}>
Re-render Components
</button>
{toggle ? <Profile /> : <Dashboard />}
</div>
);
}
Profile.js utilise l’HOC de classe :
import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";
class Profile extends Component {
render() {
return <h2>Profile Component</h2>;
}
}
export default withRenderLogger(Profile);
Dashboard.js utilise l’HOC de fonction :
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
Le module d’HOC de classe :
import React, { Component } from "react";
export function withRenderLogger(WrappedComponent) {
return class extends Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentDidUpdate() {
console.log(`${WrappedComponent.name} updated`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Et le module d’HOC de fonction :
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
Ces fichiers fonctionnent dans n’importe quelle configuration React standard ; avec Vite, nommez les fichiers JSX .jsx.
Pourquoi les hooks personnalisés ont remplacé la plupart des HOC
Les hooks personnalisés couvrent désormais la plupart des anciens cas d’usage des HOC : chargement de données distantes, vérifications d’autorisation, suivi, écouteurs DOM et abonnements. Le générateur d’entrées devient alors un hook :
function useLogger(name) {
useEffect(() => {
console.log(`${name} rendered`);
});
}
function Dashboard() {
useLogger("Dashboard");
return <h2>Dashboard Component</h2>;
}
Les hooks n’ajoutent pas de couches d’enveloppement, retournent des valeurs au lieu d’injecter des propriétés, et permettent une composition sans conflits. En l’absence d’array de dépendances, l’effet s’exécute après chaque commit.
Les HOC restent adaptés à l’enveloppement conditionnel (affichage d’une page de connexion au lieu du composant), aux bibliothèques disposant d’API HOC ainsi qu’au code basé sur des classes ; React.memo a la même structure. Appliquez les HOC au niveau du module : créer un enveloppeur pendant le rendu entraîne un remontage du sous-arbre et la perte de son état.
Points clés
- Un HOC enrobe un composant et lui renvoie une version améliorée.
- Les composants de classe comptaient sur les HOC car partager la logique de cycle de vie était sinon répétitif.
- Avec les composants fonctionnels, un HOC n’est qu’une fonction qui renvoie un composant fonctionnel.
- Les hooks personnalisés constituent aujourd’hui le choix par défaut pour réutiliser de la logique ; conservez les HOC pour l’enveloppement et le rendu conditionnel.
displayName, évitez les effets secondaires pendant le rendu, et ne créez jamais de HOCs lors du rendu.