Modèles de conception React : des OOP classiques aux hooks modernes
Explique comment les modèles de logiciel classiques tels que Singleton, Factory et Observer s’appliquent dans React, ainsi que les modèles spécifiques à React comme les HOC, les Hooks et les composants composés.
De nombreux développeurs novices en React ne réalisent pas immédiatement que les patterns de conception s’appliquent également en dehors des systèmes backend. Il est courant de penser que ces concepts ne concernent que l’architecture côté serveur, pour ensuite découvrir, lors du développement professionnel d’applications frontend, que de nombreux de ces patterns sont déjà appliqués instinctivement, sans qu’aucune étiquette consciente ne leur soit associée.
Les patterns de conception sont essentiellement des modèles éprouvés et réutilisables pour résoudre des problèmes récurrents qui apparaissent sans cesse dans les projets logiciels. Lorsque vous souhaitez que votre base de code reste organisée, bien structurée et logiquement connectée, ces patterns vous fournissent un plan à suivre. Ils agissent comme des meilleures pratiques codifiées qui améliorent la qualité de votre code et prolongent sa durée de maintenabilité.
Parmi les plus grands avantages apportés par les modèles de conception figurent la réutilisabilité, la maintenabilité, l’évolutivité, ainsi que des gains en vitesse et en efficacité. Avant de nous pencher sur les modèles spécifiques à React, il est utile de revoir les modèles fondamentaux d’ingénierie logicielle qui existent bien avant les frameworks frontend.
Modèles classiques d’ingénierie logicielle
Il s’agit de modèles qui existent indépendamment de tout langage particulier et qui s’appliquent largement aux paradigmes orientés objet et fonctionnels. React lui-même s’appuie sur plusieurs d’entre eux en interne, et les ingénieurs travaillant sur le codebase de React les utilisent pour organiser l’état, coordonner les dépendances liées au cycle de vie entre les composants, réduire la taille du bundle et maintenir une logique UI complexe facile à comprendre.
Modèle Singleton
Cet état des choses garantit qu’une classe ou un objet dispose d’exactement une instance pendant toute la durée de vie d’une application, tout en offrant un point d’accès global unique à cette instance.
En frontend, le pattern Singleton est utile pour gérer des ressources partagées — comme des stores d’état centralisés, des objets de configuration applicative, des instances de suivi analytique ou un seul client API partagé.
// Singleton API Service
class APIClient {
constructor() {
if (APIClient.instance) {
return APIClient.instance;
}
this.baseURL = "https://api.example.com";
APIClient.instance = this;
}
fetchData(endpoint) {
return fetch(`${this.baseURL}${endpoint}`).then(res => res.json());
}
}
// Any module importing/instantiating this gets the exact same instance
const client1 = new APIClient();
const client2 = new APIClient();
console.log(client1 === client2); // true
Avec les modules ES modernes, il n’est plus nécessaire de mettre en place une logique manuelle de vérification d’instance — il suffit d’exporter un seul objet ou instance partagée pour obtenir automatiquement ce comportement de singleton.
// apiClient.js
export const apiClient = new APIClient(); // ES modules cache exports automatically
Pattern Factory
Le pattern Factory définit une interface pour la création d’objets, sans que le code appelant ait besoin de connaître la classe spécifique ou la fonction constructeur responsable de la création de cet objet.
Cet état d’art est particulièrement utile lorsque vous devez générer des éléments UI de manière dynamique, gérer des réponses API sous différentes formes, ou créer des abstractions qui masquent les différences entre plateformes, comme unifier la manière dont le web et les appareils mobiles traitent les événements d’entrée.
// Button Factory for dynamically rendering UI elements
function createButton(type, config) {
switch (type) {
case 'primary':
return { role: 'btn-primary', label: config.label, onClick: config.onClick };
case 'icon':
return { role: 'btn-icon', icon: config.iconName, onClick: config.onClick };
case 'link':
return { role: 'btn-link', href: config.url };
default:
throw new Error(`Unsupported button type: ${type}`);
}
}
const primaryBtn = createButton('primary', { label: 'Submit', onClick: () => {} });
Pattern d’observateur
C’est le mécanisme sous-jacent des écouteurs d’événements ainsi que des bibliothèques de gestion d’état telles que Redux, Zustand et MobX. Il convient de noter que l’API Context de React ne repose pas sur ce pattern.
class EventEmitter {
constructor() {
this.events = {};
}
// Subscribe
on(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(listener);
}
// Publish
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(data));
}
}
}
// Usage
const store = new EventEmitter();
// Component A subscribes to state changes
store.on('userLoggedIn', user => console.log(`Welcome, ${user.name}!`));
// Login Service triggers event
store.emit('userLoggedIn', { name: 'Sarah' });
Pattern de module
Cet état d’art enrobe du code dans une clôture afin que les variables et fonctions internes restent privées, ne laissant apparaître qu’une interface publique soigneusement choisie.
Au moment où les modules natifs ES6 n’étaient pas encore disponibles, c’était la technique standard pour maintenir l’objet global window propre et pour assurer une véritable confidentialité des variables en JavaScript.
// Module using IIFE (Immediately Invoked Function Expression)
const ShoppingCartModule = (function () {
// Private variable
const cart = [];
// Private function
function calculateTotal() {
return cart.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
addItem(item) {
cart.push(item);
},
getTotal() {
return calculateTotal();
}
};
})();
ShoppingCartModule.addItem({ name: 'Keyboard', price: 100 });
console.log(ShoppingCartModule.getTotal()); // 100
console.log(ShoppingCartModule.cart); // undefined (Private!)
Aujourd’hui, les modules ES natifs avec import et export gèrent automatiquement le scoping, il est donc rarement nécessaire de créer soi-même une fermeture pour garder les variables privées.
// cart.js
const cart = []; // Private to cart.js file
export const addItem = (item) => cart.push(item);
export const getTotal = () => cart.reduce((sum, i) => sum + i.price, 0);
Modèles de conception de composants spécifiques à React
Les modèles ci-dessous abordent des défis propres à l’affichage de l’interface utilisateur, au partage de la logique entre composants, à la gestion de l’état dans un arbre de composants, ainsi qu’à l’évitement d’un trop grand nombre de transmissions de propriétés.
Cette section présente les modèles au niveau des composants suivants :
- le modèle HOC
- le modèle basé sur les hooks
- le modèle de composant composé
- la séparation entre conteneur et composant présentationnel
- l’approche des render props
- le modèle d’interface utilisateur basé sur l’IA émergente
Modèle HOC
Le Higher Order Component, ou HOC, est l’une des premières techniques proposées par React pour gérer les problématiques transversales. Prenons l’exemple où, une fois qu’un utilisateur a choisi d’être suivi, il est nécessaire de déclencher un événement d’analyse dès que le composant est chargé. Coder cette logique manuellement dans chaque composant de page devient rapidement répétitif, et la mettre à jour ultérieurement — par exemple lorsque le SDK d’analyse change — se transforme en une source de problèmes pour la maintenance. Le pattern HOC existe justement pour résoudre ce type de problème.
Page reste entièrement concentré sur le rendu, tandis que l’enveloppement withAnalytics(Page) gère séparément la logique de suivi.
Dans les bases de code plus récentes, les hooks ont en grande partie pris en charge cette responsabilité, mais vous rencontrerez encore fréquemment le schéma HOC dans les projets React anciens.
Schéma des hooks
Peu d’ajouts ont changé React autant que les hooks. Introduits dans React 16.8 en 2019, ils sont depuis devenus l’approche par défaut pour écrire la majeure partie du code React moderne.
Schéma composé
Pattern de conteneur / de présentation
Ce pattern assure une séparation claire des responsabilités en divisant les tâches d’un composant en deux rôles distincts : l’un gère la logique de l’application, et l’autre se concentre exclusivement sur l’affichage de l’interface utilisateur.
Pattern des render props
Avec ce pattern, une fonction est transmise en tant que prop à un composant, permettant à ce dernier de gérer l’état et la logique, tout en laissant à celui qui l’utilise la décision de ce qu’il faut afficher.
L’idée fondamentale derrière les render props est que, au lieu que le composant conteneur affiche lui-même une interface utilisateur codée en dur, il exécute sa logique interne puis invoque une fonction passée en prop pour générer le JSX correspondant.
Dans React contemporain, les hooks personnalisés ont pour l’essentiel remplacé les render props, qui servaient à partager de la logique de données pure. Néanmoins, les render props restent utiles lorsque un composant doit gérer et contrôler tout un sous-arbre tout en permettant à l’appelant de décider du markup. Les bibliothèques UI sans interface graphique telles que React Aria et TanStack Table s’appuient sur cette approche pour implémenter des fonctionnalités complexes — gestion de l’accessibilité, gestion du focus et autres aspects similaires — sans imposer de style ou de structure DOM particulière.
Modèle UI IA
Ce point fait partie de la liste depuis relativement peu de temps. La création d’interfaces pilotées par l’IA, qu’il s’agisse de chatbots ou d’assistants intelligents plus généraux, exige une coordination minutieuse entre les services d’IA en arrière-plan et la couche d’interface utilisateur réactive. Le modèle UI basé sur l’IA consiste essentiellement à relier des backends de grands modèles de langage à des interfaces clients réactives, afin qu’elles puissent gérer sans problème les échanges conversationnels, les réponses en flux continu et l’exécution asynchrone des modèles.
L’une des idées fondamentales ici est de séparer le backend et la couche proxy du client. Afin d’éviter de divulguer des clés API et de gérer correctement la charge de calcul, toutes les appels liés à l’IA doivent passer par une couche côté serveur — comme des Next.js Route Handlers ou un proxy API Node.js situé devant Vite. Il convient d’éviter complètement d’appeler directement les services d’IA depuis le navigateur.
Lectures complémentaires
- Trois patterns TypeScript qui améliorent l’architecture des applications React — Découvrez comment les patterns Repository, Observer et Builder utilisent le système de types de TypeScript pour créer des bases de code React et Next.js plus propres et plus faciles à maintenir.
- Typage des hooks React : useState, useEffect, useReducer et hooks personnalisés — Apprenez comment typifier correctement useState, useEffect, useReducer et les hooks personnalisés en TypeScript, ainsi que quand choisir TypeScript plutôt que du JavaScript pur est vraiment avantageux.