Introduction
En tant que développeurs, nous savons que les projets évoluent constamment : les besoins changent, les designs se métamorphosent et les spécifications initiales peuvent rapidement devenir obsolètes.
Face à cet environnement mouvant, nos composants traditionnels montrent parfois leurs limites. Ils ne sont pas tous adaptés pour faire face de manière flexible et robuste à cette évolution constante.
Qui n'a jamais été frustré par un composant trop rigide pour s'accommoder d'un changement de maquette ou d'une mise à jour des exigences du projet ?
Examinons ensemble, deux exemples, pour illustrer le Design Pattern : Compound Components.
Exemple d’un composant d’UI simple ✏️
Supposons que nous devons créer un composant Card tout ce qu’il y a de plus classique. On a besoin d’affiche un title, une description et un thumbnail.
Voilà une implementation simple de ce que pourrait être ce composant :
Ainsi que son usage :
Jusque là, tout va bien, notre composant est simple à développer, simple à utiliser et facile à relire.
Maintenant, comme dans tous les projets, le besoin évolue et le design de nos composants avec. Admettons, que notre besoin a évolué de façon à ce qu’on ai besoin d’ajouter un bouton sur notre composant Card. Mais, ce bouton ne doit pas apparaître à tous les endroits de mon application.
Ce que l’on va retrouver dans la plupart des projets professionnels aujourd’hui, c’est une surcharge de propriétés sur le composant. Le plus souvent, notre composant serait comme suit :
NB : c’est volontairement exagéré pour mettre en avant le problème. Même sans Compound Components que l’on verra après, on pourra avoir un composant bien plus propre !
Et l’usage du composant serait comme suit :
Observations
Que peux-tu observer sur cet exemple de composant “traditionnel” qui ne représente que de l’UI ?
- Structure rigide
Le composant Card a une structure définie, il contient toujours une image, un titre, une description et un bouton. Il n’y a flexibilité pour changer la structure d’un composant en fonction des besoins.
- Passage de props
Toutes les données dont le composant Card a besoin sont passées via les props. Cela peut devenir encombrant et difficile à maintenir à mesure que nous ajoutons plus de props au composant.
- Moins de réutilisabilité
Les sous-composants ne peuvent pas être réutilisés indépendamment. Par exemple, si nous voulons utiliser seulement le bouton ou l'image de la carte dans un autre composant, cela ne serait pas possible.
- Peu extensible
Ajouter de nouvelles fonctionnalités à la carte nécessite une modification de l'implémentation de la carte elle-même, augmentant potentiellement le risque de créer des bugs non liés.
- Simplicité
Cependant, dans certains cas, cette approche peut être préférable pour sa simplicité. Si votre composant est très simple et n'a pas besoin des avantages offerts par le pattern de Compound Components, le surcoût en complexité peut ne pas en valoir la peine.
Exemple d’un composant plus complexe ✏️
Supposons maintenant que nous devons créer un composant plus complexe, des composants mêmes, qui ont besoin de travailler ensuite pour mettre en oeuvre une fonctionnalité de Todo-list.
Pour cela, nous allons avoir les composants TodoList (pour afficher une liste d’item de todo), TodoItem (qui représente un item de todo), TodoForm (qui représente le formulaire d’un item de todo) et TodoStats (qui affiche des statistiques pour une liste de todo donnée).
Voilà une implementation de ce que pourrait être ces composants :
Ainsi que l’usage de ces composants :
Observations
Que peux-tu observer sur cet exemple de composant “traditionnel” qui ne représente cette fois une fonctionnalité plus complexe ?
- Rigidité
Dans l'état actuel, la structure est assez rigide. Par exemple, si vous voulez une autre variante de TodoItem qui a un bouton pour marquer une tâche comme terminée, ou peut-être une variante de TodoForm qui a des champs supplémentaires, l'adaptation de ces composants à ces scénarios serait plus complexe.
- Passage de props
Les fonctions de suppression et d'ajout sont transmises en tant que props aux composants enfants TodoItem et TodoForm depuis le composant parent HomeScreen. Cela peut devenir compliqué à gérer à mesure que l'application s'agrandit, car chaque fois que vous voulez utiliser ces fonctions, vous devez les transmettre à travers tous les composants intermédiaires.
- Manque d’encapsulation
Les composants TodoItem et TodoForm exposent trop de détails d'implémentation. Par exemple, TodoItem a besoin de connaître non seulement le contenu de la tâche, mais aussi son id et comment traiter une action de suppression. De même, TodoForm doit gérer son propre état et savoir comment gérer une action de soumission. Cela pourrait être évité avec une version composée qui masquerait ces détails.
- Peu extensible et peu lisible
Ce point est suffisamment explicite je pense !
Le Design Pattern : Compound Components 👀
Le Design Pattern : Compound Components s’applique à n’importe quel langage fonctionnant avec des composants et de la gestion d’états. Il s’agit d’une approche qui offre :
- Structure
Le terme "Compound Components" décrit une relation "a un" entre les composants. Un composant comporte plusieurs sous-composants qui travaillent ensemble pour former une unité cohérente. Le composant parent sert de composant de mise en page tandis que les sous-composants déterminent le contenu.
- Flexibilité
Les Compound Components offrent une grande flexibilité dans l'arrangement des sous-composants. Les utilisateurs de cette API de composant peuvent contrôler l'organisation, la structure et la présentation d'un composant.
- Abstraction
Ils permettent une bonne séparation des préoccupations car chaque sous-composant traite une fonctionnalité particulière. Cela permet une meilleure réutilisation des composants et simplifie le test et la maintenance.
- Pas de passage de props
Un avantage majeur du modèle de Compound Components est l'évitement du prop-drilling, qui est un problème où des props doivent être passés à travers de nombreux niveaux de composants. Les Compound Components résolvent ce problème en utilisant le contexte React pour partager la valeur entre les composants.
- Encapsulation
Avec les Compound Components, nous pouvons exposer ce qui est nécessaire et masquer les détails d'implémentation spécifiques. Cela aide à produire un code plus clair et plus facile à maintenir.
Mise en pratique
Maintenant, voyons ensemble un refactor de nos composants précédents en version Compound Components.
Le composant d’UI simple en Compound Components
Reprenons notre composant d’UI simple et convertissons les props title, description, etc. en sous-composants pour en faire une composition comme suit :
Et maintenant l’usage :
Observations
Quelles observations peux-tu faire cette fois ci ?
- Flexibilité
Le Compound Components donne un plus grand contrôle sur l'organisation des éléments dans le rendu. Dans le deuxième exemple d'utilisation, nous avons de l'information supplémentaire et une absence de bouton, ce qui ne serait pas possible avec une version non composée du composant qui limiterait strictement la structure.
- Réutilisabilité
Les sous-composants, comme CardTitle, CardImage, et CardContent peuvent être réutilisés et réarrangés librement. Cette approche réduit la duplication du code et accroît la maintenabilité.
- Lisibilité
Le code est plus facile à comprendre. Alors qu'un composant non composé pourrait avoir un grand nombre de props, ce qui pourrait rendre le code plus difficile à suivre, chaque sous-composant sait clairement quel est son rôle dans le composant de carte.
- Isolation
Les sous-composants (comme CardButton ou CardImage) peuvent être mis à jour indépendamment des autres sous-composants, évitant ainsi les effets de bord inattendus.
- Scalabilité
Les nouveaux sous-composants peuvent être ajoutés facilement en suivant cette approche, permettant au composant de s'adapter et de se développer avec le temps. Par exemple, un sous-composant CardFooter pourrait être ajouté si besoin.
Le composant complexe en Compound Components
Enfin, passons au plus intéressant, le groupe de composant qui représente la fonctionnalité de Todo-list, voilà la version Compound Components :
Ainsi que son usage, drastiquement simplifiée :
Observations
Que peux t-on observer sur cette dernière partie ?
- Flexibilité d’affichage
Avec l'approche de Compound Components, la disposition des composants est beaucoup plus flexible. Vous pouvez choisir de rendre Todos.List, Todos.Form, et Todos.Stats dans n'importe quel ordre ou même de ne pas les afficher en fonction des spécificités des spécifications ou des besoins de votre application.
- Utilisation du Contexte
Grâce à l'utilisation de React Context (TodosContext), vous pouvez facilement partager des données (todos) et des fonctions (add, remove) entre tous les composants enfants. Cela permet d'éviter le problème de prop-drilling propre à l'approche non compound components.
- Hook personnalisé
Ils utilisent un hook personnalisé useTodosContext pour obtenir les valeurs du contexte. Ce hook rend le code plus lisible et plus facile à utiliser.
- Réutilisabilité accrue
Les composants sont désormais plus indépendants et peuvent être facilement réutilisés ailleurs dans l'application. Par exemple, Todos.List pourra être utilisé dans un autre écran ou dans une sidebar sans avoir besoin de passer d'informations supplémentaires via les props.
- Extensibilité
Avec cette approche, vous pouvez également étendre facilement le composant Todos en ajoutant des sous-composants supplémentaires sans bouleverser l'architecture existante. Par exemple, si vous voulez ajouter une fonctionnalité pour marquer les tâches comme faites, vous pourriez créer un nouveau sous-composant Todos.Checkbox.
Le mot de la fin 👋
De manière générale, le composant “traditionnel” est plus simple, mais il offre moins de flexibilité et de potentiel de réutilisation que le Compound Components. Le choix entre les deux approches dépend des besoins spécifiques du projet. Mais de mon experience, partir direct sur du Compound Components est rarement une mauvaise idée !
Les inconvénients potentiels de cette approche sont qu'elle est plus complexe et qu'elle nécessite une compréhension plus approfondie des concepts de React (pour le cas de React), tels que le Contexte et les Compound Components eux-mêmes. De plus, il est important de noter que bien que le Context puisse sembler être une solution à tous les problèmes, il doit être utilisé avec parcimonie pour éviter un couplage excessif entre les composants de votre application.
L'adoption du pattern Compound Components dans la conception d'interfaces utilisateur peut sembler déroutante au début, mais les avantages qu'elle offre en termes de modularité, de flexibilité et de réutilisabilité sont indéniables. Ainsi, en décomposant intelligemment les composants en des sous-éléments logiques, nous pouvons produire des systèmes d'UI flexibles, réutilisables et gérables.
Vous pouvez retrouvez cet article au format vidéo sur YouTube en suivant ce lien.