Ce TP est l'occasion de découvrir les concepts de base, les bonnes pratiques, ainsi que les outils associés au développement d'applications en React, largement plébiscités par la communauté.
Le travail consistera à mettre en place un environnement de développement pour React. En plus de l'installation des outils, vous commencerez à manipuler la bibliothèque pour en comprendre les concepts de base.
Les notes des TP seront obtenues à partir de vos dépôts Git, vous devrez donc prendre grand soin de la qualité de ces dépôts et de leurs « commits ».
De manière similaire, les descriptions de vos « commits » devront être claires et informatives, afin de permettre l'évaluation de la progression de votre travail.
La communauté React fait un vrai effort de pédagogie, et le site de React propose une documentation bien faite pour introduire les concepts de la bibliothèque. Notamment, la section « Aperçu » propose une présentation très progressive des concepts de React. Vous allez mettre en pratique ces concepts dans la suite du TP, mais vous pourrez toujours y revenir pour assoir votre compréhension des concepts de la bibliothèque.
Le système de fichier du département informatique étant un peu poussif sur les accès aux fichiers. Et les projets React sont constitués de nombreux fichiers. Vous êtes encouragé à travailler dans le répértoire « /working » de votre machine. Ce répertoire est un répertoire local à la machine, il n'utilise donc pas le système de fichier du département. En contrepartie, votre projet ne sera pas sauvegardé sur les serveurs du département et ne sera accessible que de cette machine. Vous penserez impérativement à sauvegarder votre travail sur le serveur GitLab au moins à chaque fin de séance.
De plus toujours pour ne pas solliciter le système de fichier du département, vous pourrez déplacer le cache du gestionnaire de paquets de Node afin qu'il ne soit plus sur votre compte, mais dans le répertoire temporaire de la machine.
Afin de pouvoir mettre en pratique un peu de code, vous allez maintenant mettre en place un projet npm pour gérer notre projet. Vous commencerez par créer un nouveau répertoire de travail pour le module que vous appellerez React.
Il est tout à fait possible de mettre en place un projet React « à la main », mais l'installation et la configuration des différents paquets requis se révèle vite fastidieuse.
Pour pallier cela, la communauté React à mis au point un outil permettant d'installer un ensemble d'outils de base et de les configurer afin de répondre aux bonnes pratiques du développement d'application en React : create-react-app
.
Il s'agit souvent du départ par défaut d'une application React. Cependant, create-react-app
installe de nombreuses dépendances rendant le projet très volumineux. Nous allons utiliser une alternative plus récente et plus légère : vite
.
Vous allez initialiser votre projet à l'aide de la commande suivante :
npm init vite r410-introduction
Cette commande permet d'initialiser un projet npm, puis d'exécuter la commande vite
permettant de définir les dépendances du projet en fonction du type de projet. Dans notre cas :
Puis comme indiqué, vous pouvez ensuite vous rendre dans le répertoire du projet et installer toutes les dépendances :
cd r410-introduction npm install
Cet outil permet de créer une structure de projet React fonctionnelle. Vous pourrez détailler le contenu des fichiers et répertoires créés avec votre intervenant de TP ou encore avec la documentation.
Vous contrôlerez que tout fonctionne correctement en lançant le serveur de développement npm run dev. Vous pouvez consulter le fichier package.json pour une liste des commandes.
Dans le répertoire src, vous supprimerez l'ensemble des fichiers à l'exception des fichiers « App.jsx », « main.jsx » et « index.css ». Vous remplacerez ensuite le contenu des fichiers « App.jsx » et « index.css » en y copiant les contenus suivants : App.jsx (télécharger) et index.css (télécharger)
Vous devriez obtenir une application à nouveau fonctionnelle dans votre navigateur.
Vous pourrez modifier un peu le contenu du composant App
et constater le rechargement « à chaud » de l'application.
Vous finaliserez la mise en place du projet en créant un dépôt Git local dans le répertoire r410-introduction. Vous créerez alors un dépôt distant associé, nommé r410-introduction, puis vous réaliserez votre premier commit.
git init --initial-branch=main git remote add origin https://iut-info.univ-reims.fr/gitlab/VOTRE_LOGIN/r410-introduction.git git add . git commit -m "Initial commit" git push -u origin main
Vous penserez à ajouter votre intervenant de TP comme membre du dépôt distant, avec un rôle au moins de Reporter.
Vous allez ajouter un « linter » à votre projet à l'aide d'ESLint. ESLint
est un « linter », largement répandu dans la communauté JavaScript, car il propose un grand degré de configuration à l'aide de nombreuses règles.
Cependant, afin de ne pas configurer vous-même toutes les règles d'ESLint
, vous allez générer un fichier de configuration s'appuyant sur un ensemble de règles prédéfini :
npx eslint --init
ESLint
vous pose quelques questions pour configurer l'environnement :
Le fichier de configuration (.eslintrc.cjs) est fonctionnel, mais il ne supporte pas Prettier. Pour ajouter le support de Prettier, vous installerez les modules suivants :
npm install --save-dev eslint-config-prettier eslint-plugin-prettier prettier
Puis vous pourrez utiliser le fichier de configuration suivant comme base de configuration pour ESLint : .eslintrc.cjs (télécharger)
Vous devriez constater dans votre éditeur plusieurs erreurs sur les fichiers sources de l'application. Ces erreurs ne sont pas nécessairement graves mais elles ont vocation à imposer des règles permettant de renforcer la cohérence et l'uniformité du code.
Vous devez faire en sorte de résoudre ces erreurs.
Nous avons choisi un jeu de règles par souci de simplicité, mais si une règle ne vous convient pas, vous pouvez et devez modifier la configuration d'ESLint pour le refléter.
Dans la suite des TP, vous prendrez soin de résoudre tous les problèmes d'ESLint et de Prettier.
Il existe de nombreuses façons de gérer le style des composants de React. Par défaut la commande vite
encourage l'import de CSS dans le JavaScript. Cette pratique permet de décomposer le CSS avec une vision par composant. Cependant, cette simplification est obtenue au prix d'une complexification de la structure du projet.
Afin de simplifier l'introduction aux concepts de React et afin de nous ramener à une approche plus classique, vous placerez tous les fichiers non compilés, les « assets » (image, video, son, …), dans le répertoire public. Et vous utiliserez le fichier CSS index.css comme unique source CSS.
Vous déplacerez donc le fichier CSS index.css dans le répertoire public en ajoutant son utilisation dans le fichier index.html du projet et vous supprimerez son import dans le fichier main.jsx.
React est une bibliothèque reposant sur les paradigmes du développement orienté composant, vous allez donc ajouter un premier composant à votre application.
Vous ajouterez le fichier Card.jsx (télécharger) suivant dans un sous-répertoire components du répertoire src :
Il s'agit de l'implémentation la plus simple d'un composant que l'on puisse réaliser. Vous pourrez noter l'usage de JSX pour simplifier l'écriture du composant.
Ce type de composant est appelé composant-fonction, il existe un autre type de composant reposant sur l'utilisation de classe, dit composant-classe, mais ils ont l'inconvénient d'être plus verbeux dans leur écriture. Ces composants-classes ont longtemps été nécessaires pour la gestion de données par les composants, cependant, depuis sa version 16.8, React propose une alternative pour les composants-fonctions. Dans le cadre de ce cours, nous n'utiliserons que des composants-fonctions.
Pour commencer, en vous inspirant de l'import et de l'utilisation du composant App
dans le fichier main.jsx, vous importerez le composant Card
dans le module App.jsx et vous ajouterez ensuite 4 instances de ce composant dans le rendu de l'application grâce à la syntaxe JSX.
Pour obtenir le rendu suivant, vous encapsulerez les cartes dans un élément HTML div
comportant la classe CSS cards
.
Card
.
Card
depuis le composant App
.
Nous avons maintenant 4 cartes affichées dans l'application, parfaitement alignées et identiques. Même s'il est intéressant de pouvoir regrouper une structure HTML dans un composant, il serait beaucoup plus intéressant de pouvoir la personnaliser pour chaque instance.
En imaginant que vous souhaitiez modifier le titre de la carte, vous allez devoir modifier le contenu du composant. Pire encore, si l'on souhaite obtenir des cartes avec différents titres, le composant doit être dupliqué pour chacun des titres, ce qui révèle une conception malheureuse.
En réalisant une analogie avec des fonctions, nous identifions rapidement comment factoriser le code. Pour l'instant, nous avons des composants similaires à ces fonctions, très spécialisées :
La solution consiste à rendre générique la fonction en acceptant un nouveau paramètre, correspondant à la valeur à ajouter. De cette manière, toutes les fonctions d'addition spécialisées peuvent être remplacées par une seule fonction plus générique :
Sur le même principe, il est possible de passer des paramètres aux composants de React, qui seront appelés des « props ».
En JSX, la transmission d'une props à un composant se fait en utilisant la syntaxe des attributs HTML. Par exemple, dans le cadre suivant, l'attribut value
sera associé à "une valeur"
et text
à "Une autre valeur"
et seront tous les deux transmis au composant UnComposant
:
Dans le composant React
, l'ensemble des props transmis à un composant sera regroupé dans un objet JavaScript et passé en paramètre du composant-fonction concerné. Avec le code précédent, lors de l'instanciation du composant le paramètre props
aurait la valeur suivante :
Dans un premier temps, nous allons nous intéresser au titre des cartes, vous commencerez par ajouter un paramètre props
à votre composant Card
que vous utiliserez pour obtenir une propriété title
contenant le texte du titre à afficher dans le bouton. Vous noterez l'usage des accolades au sein du JSX. Il s'agit d'une manière pratique d'intégrer des expressions JavaScript dans le JSX.
Vous devriez obtenir 4 cartes sans titre dans votre application puisqu'aucune valeur n'a été affectée au paramètre title
lors de la création du composant dans le composant App
. Vous modifierez les composants Card
du composant App
afin d'obtenir un rendu similaire à celui-ci :
ESLint devrez vous indiquer une erreur sur l'utilisation de la props title
dans votre composant. En effet, les bonnes pratiques voudraient que vous contrôliez la présence et le type de cette props. Vous pouvez ignorer cette erreur pour l'instant, vous allez la résoudre dans les prochaines sections.
title
au composant.
title
pour spécialiser les instances de Card
.
L'ensemble des propriétés du composant est regroupé dans un unique objet passé en paramètre du composant. Afin d'alléger la syntaxe d'accès aux propriétés du composant, il est courant d'utiliser l'affectation par décomposition (« destructuring assignment »), qui permet de créer à la volée des variables correspondantes aux propriétés souhaitées :
Dans l'exemple précédent, toutes les propriétés reçues sont transférées à l'élément HTML input
, il est possible de simplifier la transmission des propriétés à l'aide de l'opérateur de décomposition (« spread operator ») :
Cette pratique n'est cependant pas encouragée dans le cadre des bonnes pratiques, l'utilisation de la délégation de contenu, que nous verrons juste après, lui est généralement préférée.
Cependant son usage n'est pas rare et il est courant de le retrouver dans du code, et grâce au reste de la décomposition, il est possible de combiner ces deux syntaxes :
En vous inspirant des exemples précédents, vous décomposerez le paramètre props reçu dans le composant Card
afin d'en extraire le titre.
ESLint provoque toujours une erreur sur la decomposition des props, c'est dans la section suivante que vous résolvez le problème.
Card
en extrayant le titre des props reçus en paramètre.
Avant de passer à la suite, revenons sur le cas des cartes avec un titre vide. JavaScript est un langage interprété, c'est-à-dire que le langage est directement lu et exécuté par le navigateur, sans avoir à passer par un langage compilé intermédiaire. Le langage ne dispose donc pas de moyen de contrôle du code avant son exécution pour détecter, par exemple, qu'un attribut est requis par un composant.
Afin d'améliorer la gestion des props et de rendre l'utilisation d'un composant plus robuste, il est recommandé d'utiliser des outils de contrôle des props comme le module PropTypes. Celui-ci vous permettra de lister les propriétés d'un composant, d'en assurer une description précise et même d'en définir des valeurs par défaut si nécessaire. Dans le cas présent, vous définirez la propriété title
comme une chaîne de caractères avec valeur par défaut « Title ».
Par défaut, le module React d'ESLint impose que les valeurs par défaut soient spécifiées dans une propriété defaultProps du composant conjointement à la propriété propTypes. Si vous préférez la version reposant sur la valeur par défaut des paramètres du composant, vous modifierez la règle d'ESLint conformément à la documentation en ajoutant la règle suivante dans votre « .eslint.cjs » :
Il est à noter que les PropTypes n'empêchent pas l'application de fonctionner, mais réalisent des avertissements dans la console.
À partir de maintenant, vous utiliserez systématiquement PropTypes pour contrôler tous les composants ayant des propriétés.
Les éditeurs peuvent de temps en temps afficher une erreur sur l'import d'une nouvelle dépendance, comme par exemple prop-types
même lorsque celui-ci était défini dans les dépendances du projet.
Un simple redémarrage permet généralement de régler le problème.
Afin de rendre les composants les plus génériques possible, les bonnes pratiques React encouragent la délégation de contenu. Il s'agit de permettre à des composants de recevoir comme propriété (props) un contenu à afficher au sein du composant. Cette démarche permet de déléguer à l'utilisateur du composant le contenu d'une ou plusieurs parties de son contenu.
Dans notre cas, le composant Card
que vous venez de créer pourrait afficher un paragraphe de texte avec une image, un carousel d'images ou une vidéo. Si l'affichage du contenu de la carte est géré directement dans le composant, alors il faudra faire un affichage spécifique en fonction du type de paramètre passé : un tableau d'image, une vidéo, du texte… Et si un nouveau type d'affichage doit être ajouté, alors le code du composant doit être mis à jour.
Une solution plus élégante consiste à recevoir directement le contenu à afficher sous forme d'une props et de l'ajouter telle quelle au rendu JSX du composant, rejetant alors la responsabilité du contenu sur l'utilisateur du composant.
Ici le code du composant Card
ne connait pas la nature du contenu qu'il doit afficher, sa responsabilité concerne uniquement l'affichage de la carte.
Cette pratique illustre le fait qu'il est tout à fait possible de fournir du JSX à une propriété d'un composant qu'il pourra alors insérer dans son propre JSX.
Dans le cas où il n'y a qu'un contenu à transmettre au composant, il est généralement plus naturel d'utiliser la propriété spéciale children
pour transmettre le contenu du composant. Le contenu du composant est alors défini par ce qui est encadré entre les balises ouvrante et fermante du composant.
Vous modifierez le composant Card
pour initialiser le contenu de la carte à l'aide de la props children
.
Pour résoudre les soucis d'ESLint qui devraient apparaître dans le composant Card
vous typerez la props children
avec le type node
et avec la valeur par défaut « Content »
Vous en profiterez pour donner le type node
à la props title
afin d'autoriser du balisage dans le titre de la carte.
Vous modifierez les composants Card
du composant App
afin d'obtenir un rendu similaire à celui-ci :
children
pour définir le contenu du composant Card
.
Card
.
Card
dans l'application.
Avant de poursuivre, vous allez créer un nouveau composant Button
. Il s'agit d'un composant relativement simple, mais c'est un classique car il permet de normaliser les boutons sur l'ensemble de l'application.
D'un point de vue pédagogique, il va vous permettre d'approfondir votre compréhension des composants.
Vous allez créer un composant Button
qui recevra comme props :
children
, de type node
et null par défaut
className
, de type chaîne de caractères, vide par défaut.
Ce composant retournera un élément HTML button
contenant comme classe CSS la props className
avec classe CSS btn
. Le bouton contiendra entre ces balises le contenu de la props children
.
Vous ajouterez une instance de ce composant aux deux premières cartes de votre application, pour obtenir un rendu similaire à celui-ci :
Button
.
Button
dans le composant App
.
Vous allez mettre en place des compteurs de clics. Et afin de leur donner un peu de personnalité, vous allez leur ajouter une petite icône.
Les polices d'icônes sont devenues courantes dans le monde du web. Dans le cadre de React, elles sont généralement encapsulées dans des bibliothèques de composants.
Vous allez utiliser les icônes Font Awesome, mais pour y avoir accès, vous allez commencer par installer les paquets npm :
npm install @fortawesome/fontawesome-svg-core \ @fortawesome/free-solid-svg-icons \ @fortawesome/react-fontawesome
Une fois les paquets installés, vous pourrez afficher une icône dans chaque bouton, comme heart
ou star
, par exemple, ou n'importe quel autre icône.
Button
.
Pour l'instant, les composant Button
ne sont pas interactifs. Vous allez ajouter un écouteur d'événements sur le clic de la souris pour pouvoir y remédier.
Comme précisé dans la documentation, l'ajout d'un écouteur d'événements en React est très similaire à la syntaxe HTML.
Dans un premier temps, dans le composant Button
, vous attacherez une fonction de rappel réalisant l'affichage d'un message dans la console de développement lors du clic de la souris.
Vous constaterez que cette solution ne permet pas d'avoir un message différent pour les deux boutons. Pour obtenir ce résultat, vous ajouterez une nouvelle props onClick
à votre composant Button
qui recevra une fonction de rappel, c'est cette fonction que vous associerez à l'événement du clic de la souris sur le bouton HTML.
Vous ajouterez ensuite une fonction de rappel aux boutons dans l'application, permettant l'affichage d'un message spécifique dans la console de développement. Le code suivant propose des exemples de fonctions de rappels affichant un message dans la console de développement.
Vous devriez maintenant voir apparaitre un message à chaque clic de souris sur un bouton de l'application.
Vous noterez au passage, que vous avez détecté le clic de la souris dans le composant Button
et que vous avez transmis cette information à l'aide d'une fonction de rappel à un composant parent, ici App
. Nous reviendrons sur cette mécanique, mais il s'agit du procédé classique pour faire remonter des informations d'un composant fils vers son parent.
onClick
dans le composant Button
.
onClick
au composant Button
.
onClick
du composant Button
dans le composant App
.
Maintenant que vous disposez de l'information du clic de la souris, vous allez pouvoir mettre en place des compteurs de clics.
Dans un premier temps, dans le composant App
, vous ajouterez une variable cpt
initialisée à 0 :
Dans les fonctions de rappels transmises à la props onClick
des instances de composant Button
:
et pour finir, vous ajouterez l'affichage de la valeur de la variable
En essayant de cliquer sur les boutons, vous constaterez dans la console de développement que la valeur de la variable cpt
est bien incrémentée, mais l'affichage des composants Button
n'est pas mise à jour.
Pour remédier à ce problème, il faut introduire la notion d'état de composant. En effet, pour qu'un composant soit conscient des modifications d'une variable, celle-ci doit faire partie de son état.
Historiquement, pour utiliser un état dans un composant React, il était nécessaire d'avoir recours aux « composants-classes ». Mais depuis l'introduction des hooks, il est possible d'ajouter des variables d'état aux « composants-fonctions ». Il s'agit de la solution que nous privilégions dans les TP.
L'utilisation du «hook d'état » permet de définir des variables spéciales, qui sont associées à l'état du composant dans lequel ils sont définis. Lorsque ces variables seront modifiées, React provoquera le rafraîchissement de l'affichage du composant associé.
L'appel à useState
retourne un tableau avec la variable dans la première case et la fonction de modification dans la seconde. Vous pouvez initialiser la valeur de la variable en passant une valeur en paramètre de la fonction useState
.
Vous devez impérativement utiliser la fonction de modification retournée par useState
pour modifier la valeur de la variable, car c'est l'appel à cette fonction qui informe React qu'il doit mettre à jour le composant.
Sur ce principe, vous ajouterez une variable d'état cpt
à votre App
, initialisée à 0
. Et vous incrémenterez sa valeur dans la fonction de rappel attachée à la props onClick
.
Vous devriez constater que maintenant la valeur affichée dans les boutons augmente lorsque ceux-ci sont cliqués.
cpt
pour compter les clics des boutons.
cpt
dans l'application.
Vous constaterez que les deux boutons partage la même variable d'état cpt
. Si vous souhaitez deux compteurs différents, vous devez créer une nouvelle variable d'état qu'il va falloir associer au deuxième bouton.
Cette solution est fonctionnel, mais si vous devez gérer plusieurs compteurs, elle devient vite fastidieuse. Il est alors plus intéressant de créer un nouveau composant spécialisé, qui intégrera la logique associée, dans notre cas : un compteur.
Vous allez créer un nouveau composant Counter
qui
cpt
, qui contiendra la valeur du compteur,
before
, une props de type node
, null
par défaut, contenant ce qui doit être affiché avant le compteur
after
, une props de type node
, null
par défaut, contenant ce qui doit être affiché aprés le compteur
className
, une props de type string
, vide par défaut, contenant les classes CSS à ajouter au bouton.
Vous remplacerez les instances du composant Button
de l'application dans le composant App
.
Counter
.
Counter
dans le composant App
.
Vous disposez maintenant de deux compteurs indépendants, et leur mise en place est simplifiée, car ils ont internalisé le compteur et sa logique.
Mais imaginons que l'on souhaite afficher le total des compteurs dans la troisième carte. Vous devez de nouveau avoir accès à la valeur des compteurs depuis le composant App
.
Comme vous l'avez vu dans la section « Ajout d'interaction », pour faire remonter une information d'un composant vers son parent, vous devez utiliser une fonction de rappel en transmettant en paramètre les données à faire remonter.
Vous ajouterez donc, au composant Counter
, une props onChange
de type fonction et null
par défaut.
Cette props doit être invoquée chaque fois que le la valeur du compteur est modifiée. Évidement, il est possible de l'invoquer lors de chaque utilisation de la fonction de modification de la variable d'état. Mais React propose un hook qui enregistre une fonction de rappel à invoquer lors du changement de valeur d'une des variables transmise en dépendance, sous forme d'un tableau en deuxième paramètre. Il s'agit du hook useEffect
.
Vous utiliserez ce hook pour invoquer la props onChange
, si elle n'est pas null
, en lui fournissant la valeur du compteur comme paramètre, à chaque fois que la valeur de la variable d'état sera modifiée.
Dans le composant App
, vous devez stoquer les valeurs des compteurs pour en afficher le total. Ces valeurs devant varier dans le temps, vous allez créer une variable d'état counts
pour recevoir ces valeurs, que vous initialiserez avec l'objet suivant, que vous utiliserez comme un tableau associatif :
Bien sûr, vous modifierez le nom des propriétés en fonction de vos icônes.
Vous afficherez ensuite la somme des compteurs dans le contenu de la troisième carte. Et vous associerez une fonction de rappel à la props onChange
. Ces fonction de rappel lors de leur invocation recevront la nouvelle valeur du compteur associé, elles devront donc mettre à jour la propriété adaptée de la variable d'état counts
. L'opérateur de destructuration est couramment utilisé dans ce cas pour réduire la syntaxe.
Vous devriez maintenant voir le total se mettre à jour automatiquement lors du clic sur les compteurs.
onChange
au composant Counter
.
useEffect
pour invoquer la props onChange
lors des modification de la variable d'état cpt
.
counts
dans le composant App
.
counts
dans les fonctions de rappels associées à la props onChange
des compteurs.
Comme vous l'avez vu précédemment, la spécialisation d'un composant consiste à créer un nouveau composant, utilisant le composant à spécialiser, en lui spécifiant certaines propriétés ou comportements spécialisés. Il s'agit d'un patron de développement (« design pattern ») couramment employé dans le développement par composant « les composants d'ordre supérieur » (High Order Component ou HOC).
Vous allez maintenant réaliser une spécialisation du composant Card
permettant d'être, au choix, replié ou déplié. Ce nouveau composant s'appellera FoldableCard
et son rendu se conformera à la maquette suivante :
Afin que le composant FoldableCard
puisse être ouvert ou fermé, vous ajouterez une props opened
au composant FoldableCard
de type booléen et valant false
par défaut.
Il est courant de devoir afficher/cacher certaines parties d'un composant compte tenu des propriétés ou de l'état d'un composant. React résout cela simplement en réalisant un simple test sur les valeurs concernées lors du rendu de l'affichage du composant. Vous pouvez recourir à l'utilisation d'un test, mais il est courant de rencontrer l'utilisation de l'opérateur ternaire ou de l'opérateur paresseux « && ».
Vous afficherez la props children
de manière conditionnelle en fonction de la props opened
.
Vous noterez sur l'illustration suivante que les cartes repliables se distingue des cartes classiques par un bord bas plus épais et que les cartes repliées ont des points de suspension sur celui-ci. Pour parvenir à ce rendu, vous devez ajouter des classes CSS au composant Card
à l'aide de la props className
. La classe CSS foldable
ajoute l'affichage des cartes repliables et la classe CSS shown
permet de supprimer les points de suspension. Vous devrez modifier le composant Card
de manière approprié pour qu'il supporte l'usage de la props className
.
En transformant les 3 dernières cartes de l'application et en ouvrant les cartes 2 et 3, vous devriez obtenir un rendu similaire à celui-ci :
FoldableCard
.
opened
au composant FoldableCard
.
className
au composant Card
.
FoldableCard
dans l'application.
Afin d'afficher l'état des cartes repliables de manière plus explicite, vous afficherez une icône circle-plus
ou circle-minus
dans l'en-tête de la carte avec la classe CSS foldable-icon
.
Vous afficherez circle-minus
lorsque la carte est dépliée et circle-plus
lorsqu'elle est repliée, avec la configuration précédente, vous devriez obtenir le rendu suivant :
La props title
du composant Card
ne peux pas recevoir plusieurs composants. Une solution consiste à les regrouper à l'aide d'une balise HTML, et si cette balise n'a pas de raison d'être dans la page, vous êtes encouragé à utiliser un « fragment ». Les fragments sont couramment utilisés en JSX, au point qu'ils ont une syntaxe courte, très pratique.
FoldableCard
.
Pour l'instant, les cartes sont initialement repliées ou dépliées, et ne changent pas d'état au cours du temps. Pour résoudre ce problème, vous ajouterez au composant FoldableCard
une variable d'état isShown
, qui sera initialisée à l'aide de la props opened
.
Vous réaliserez la permutation de sa valeur lors du clic de souris sur l'instance du Card
du composant FoldableCard
.
Lorsque vous ajouterez la propriété HTML onClick
sur l'élément HTML racine du composant Card
, ESLint requiert que l'élément HTML soit un élément interactif. En effet, pour l'accessibilité de votre application, il est recommandé d'utiliser des éléments interactif comme support de l'interaction. Vous remplacerez donc l'élément HTML racine du composant Card
, l'élément article.card
, par un bouton de type button
avec la classe CSS card
.
Vous finaliserez votre composant en remplaçant l'utilisation de la props opened
par la variable d'état isShown
.
Vous devriez maintenant être capable d'ouvrir/fermer les composants FoldableCard
à l'aide d'un clic de souris.
isShown
dans le composant FoldableCard
.
onClick
au composant Card
.
onClick
du composant Card
dans le composant FoldableCard
.
isShown
lors du clic de la souris.
Vous avez dû constater que le deuxième compteur n'est plus réellement fonctionnel, car la carte se replie à chaque fois que le bouton est cliqué. En effet l'événement du clic de la souris est d'abord intercepté par le composant Button
, puis il est propagé à tous ces ancêtres jusqu'à la racine de la page HTML, en passant par le composant FoldableCard
qui l'intercepte et replie la carte.
Pour résoudre ce souci, dans le composant Button
créer une fonction :
onClick
si celle-ci n'est pas null
.
Cette fonction pourra être transmise à la propriété HTML onClick
du bouton HTML dans le composant Button
.
Vous noterez au passage que le compteur est remis à 0 à chaque fois que la carte est replié. Ce qui s'explique par le fait que lorsque la carte est repliée, le compteur n'est pas seulement non-affiché, il est supprimé du rendu par l'affichage conditionnel. Un nouveau compteur est construit, et initialisé à 0, lorsque la carte est dépliée.
Ici, il y a deux cas de figure, avec deux problèmes différents :
Pour résoudre le premier cas de figure, React propose avec le hook useEffect
de retourner une fonction qui sera invoquée lors de la suppression du composant, vous pourrez y invoquer la props onChange
avec la valeur 0, pour signaler la remise à 0 du compteur.
À titre pédagogique, vous pouvez tester cette solution, mais dans le cadre du TP, nous allons nous focaliser sur le second cas de figure, vous veillerez donc à supprimer le retour du hook useEffect
avant de poursuivre.
Pour le second cas de figure, nous disposons déjà des valeurs des compteurs dans la variable d'état counts
du composant App
. Il ne reste plus qu'à initialiser la valeur du compteur avec cette valeur à sa création. Vous ajouterez une nouvelle props initial
, de type nombre, valant 0 par défaut, que vous utiliserez pour initialiser la variable d'état cpt
. Dans le composant App
, vous initaliserez les compteurs à l'aide de la variable d'état counts
.
Vous devriez maintenant avoir résolu les problèmes du deuxième compteur, et votre application devrait être de nouveau fonctionnelle.
Button
.
useEffect
.
counts
du composant App
.
Au cours du développement, en ajoutant des fonctionnalités aux composants, ceux-ci peuvent vite devenir chargés et peu lisibles. Afin de résoudre ce problème, il est possible de déporter une partie de la logique d'un composant à l'extérieur de celui-ci sous la forme d'un hook personnalisé.
Vous allez créer un hook personnalisé comportant la partie logique du composant FoldableCard
: useShowable
. Dans le sous-répertoire « src/hooks », vous allez créer le hook personnalisé useShowable
dans le fichier « useShowable.js ».
Les hooks ne sont finalement que des fonctions, et celle-ci accepte deux paramètres :
shown
un booléen indiquant l'état initial
baseClassName
une chaîne de caractères contenant la classe CSS de base, valant une chaîne vide par défaut.
Le hook personnalisé useShowable
crée une variable d'état et retourne un objet JavaScript comportant les propriétés suivantes :
isShown
contenant la variable d'état du composant : affiché ou caché,
toggleShown
contenant une fonction qui permutera la valeur de la variable d'état de vrai à faux et de faux à vrai à chaque invocation,
setIsShown
contenant une fonction qui permettra de fixer la valeur de la variable d'état,
extendedClassName
contenant une chaîne de caractère composé de baseClassName
suivi de la classe CSS shown
si la variable d'état est à vrai.
Vous utiliserez ensuite ce hook dans le composant FoldableCard
pour en simplifier le code. Vous pourrez utiliser la décomposition pour simplifier l'utilisation de l'objet retourné par le hook.
L'utilisation d'un hook pour simplifier le code est ici un peu extrême, puisque le code initial n'était pas réellement compliqué. Cependant, un autre avantage de l'utilisation des hooks personnalisés est la mutualisation de code. Dans le cas présent, si vous souhaitez créer un nouveau composant dont le contenu doit être affiché ou caché, comme un menu déroulant ou des onglets par exemple, vous pourrez directement réutiliser le hook personnalisé useShowable
.
useShowable
dans le sous-répertoire « src/hooks ».
FoldableCard
.
Afin de simplifier la lecture et le maintien du code des composants, les bonnes pratiques encouragent le découpage des composants pour limiter les responsabilités des composants au minimum.
Dans cette optique, vous allez créer un nouveau composant Cards
, dont le rendu pour l'instant ne comportera que l'élément HTML div
ayant la classe CSS cards
qui se trouvait initialement dans le composant App
.
Vous allez enrichir ce composant dans la question suivante.
Cards
.
Il est courant de disposer d'un tableau de données et de souhaiter en produire un tableau de composants à afficher. React propose l'affichage direct des composants du tableau lorsque le tableau JavaScript les contenant est inséré dans du JSX. Il est donc courant de voir recourir à la méthode map
pour convertir un tableau de données en un tableau de composants qui est ensuite injecté dans du JSX pour l'afficher. Cependant, pour gérer le rafraîchissement des composants, React requiert que les différents composants du tableau soient identifiés par une propriété unique : key
.
Vous ajouterez le fichier « cardsData.js » (télécharger) suivant dans un sous-répertoire « constants » du répertoire src :
Ce fichier exporte un tableau de données permettant de générer les cartes que l'on souhaite afficher.
Vous importerez ce tableau dans le composant App
et vous le transmettrez au composant Cards
à l'aide d'une nouvelle propriété cardsData
.
Dans le composant Cards
, à partir du tableau cardsData
, vous produirez un tableau de FoldableCard
que vous afficherez dans un élément HTML div
ayant la classe CSS cards
.
Vous prendrez soin de définir les PropTypes de la propriété cardsData
à l'aide de PropTypes.arrayOf
et PropTypes.shape
.
cardsData
au composant Cards
.
Cards
.
FoldableCard
à partir de la propriété cardsData
.
Vous avez conçu vos cartes repliables comme des composants autonomes, chaque carte peut se déplier/replier indépendamment des autres. Imaginons maintenant que vous souhaitiez contraindre l'ouverture des cartes pour n'avoir qu'une carte ouverte à la fois.
Dans cette optique, vous allez devoir ajouter une information déterminant quelle carte est visible. Lorsqu'une information est utile à plusieurs composants, ici toutes les FoldableCard
, la philosophie React de gestion des données préconise de remonter la gestion de l'information dans l'un des ancêtres communs à tous les composants, dans notre cas, le composant Cards
.
Dans le composant Cards
, vous ajouterez une variable d'état openedId
qui stockera l'id
de la carte dépliée dans le tableau des cartes. Vous pourrez ensuite utiliser cette variable d'état afin d'initialiser la propriété opened
des composants FoldableCard
générés.
Vous devriez obtenir au chargement de la page une carte dépliée, celle correspondant à l'indice d'initialisation de openedId
ou aucune si aucune valeur n'a été utilisée pour initialiser la variable d'état. Cependant, il est toujours possible de déplier plusieurs cartes en même temps, pour résoudre ce problème, vous allez devoir mettre à jour la variable d'état openedId
lorsqu'une carte est dépliée.
Comme vous l'avez vu dans la section « Remontée d'information au parent », pour faire remonter une information d'un composant vers son parent, vous devez utiliser une fonction de rappel en transmettant en paramètre les données à faire remonter.
Vous ajouterez donc une nouvelle propriété onToggleOpened
au composant FoldableCard
, il s'agira d'une fonction de rappel qui sera invoquée lors du changement d'état de la carte, pliée/dépliée, et acceptant en paramètre le nouvel état isShown
.
Dans le composant Cards
, vous modifierez la valeur de la variable d'état openedId
lorsque la fonction de rappel transmise à la props FoldableCard
sera invoquée.
Vous constaterez en affichant la valeur de la variable d'état openedId
dans la console de développement, que sa valeur est bien conforme aux attentes, mais qu'il est toujours possible de déplier plusieurs cartes en même temps.
Ce problème est dû au fait que vous utilisez une variable d'état (isShown
) pour gérer l'état de la carte dans le composant FoldableCard
et que la propriété opened
n'est utilisée que pour initialiser cette variable.
Une première solution consiste à supprimer cette variable d'état. Nous lui préférons une seconde approche : mettre à jour la variable d'état isShown
du composant FoldableCard
à chaque changement de la propriété opened
, en utilisant le hook d'effet : useEffect
.
Vous ferrez en sorte de mettre à jour la variable d'état isShown
à chaque fois que la valeur de la props opened
sera modifiée.
openedId
dans le composant Cards
, stockant l'id
de la carte dépliée.
onToggleOpened
au composant FoldableCard
.
Cards
utilisation de la propriété onToggleOpened
pour mettre à jour la variable d'état openedId
.
useEffect
pour mettre à jour la variable d'état isShown
.