Il s'agit d'une version statique de l'intranet d'A. Jonquet, certaines fonctionnalités sont donc potentiellement non fonctionnelles.
Rejoindre la version dynamique 🔒
React
Navigation

Introduction au « framework » React

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.

Remarque importante

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.

Objectifs de la séance

  • Mise en place d'un environnement de travail React
  • Introduction aux concepts de React
  • Introduction à la création de composants

Introduction à React

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.

Travail à réaliser
  • Lire la section « Aperçu » de l'introduction à React du site officiel.

Mise en place d'un projet React

Remarque importante

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 :

  • framework : React,
  • variant : JavaScript.

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.

Travail à réaliser
  • Mise en place de la structure du module.
  • Création du projet React à l'aide de l'outil vite.
  • Nettoyer le projet.
  • Mise en place du versionnage du projet et de la sauvegarde (r410-introduction).

Qualité de code : ESLint et Prettier

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 :

  • « How would you like to use ESLint? » : « To check syntax, find problems, and enforce code style »,
  • « What type of modules does your project use? » : « JavaScript modules (import/export) »,
  • « Which framework does your project use? » : « React »,
  • « Does your project use TypeScript? » : « No »,
  • « Where does your code run? » : « Browser »,
  • « How would you like to define a style for your project? » : « Use a popular style guide »,
  • « Which style guide do you want to follow? » : « Airbnb: https://github.com/airbnb/javascript »,
  • « What format do you want your config file to be in? » : « JavaScript »,
  • « Would you like to install them now with npm? » : « Yes »,

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.

Travail à réaliser
  • Mise en place d'ESLint
  • Mise en place de Prettier.

Gestion du CSS et des « assets »

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.

Travail à réaliser
  • Déplacement du fichier CSS index.css de src à public.
  • Ajout de l'utilisation de index.css dans index.html.

Un premier composant

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.

Travail à réaliser
  • Ajout d'un composant Card.
  • Utilisation du composant Card depuis le composant App.

Les « props »

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 :

Remarque importante

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.

Travail à réaliser
  • Ajout d'une props title au composant.
  • Utilisation de cette props title pour spécialiser les instances de Card.

Décomposition du paramètre de propriétés

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.

Remarque importante

ESLint provoque toujours une erreur sur la decomposition des props, c'est dans la section suivante que vous résolvez le problème.

Travail à réaliser
  • Modification du composant Card en extrayant le titre des props reçus en paramètre.

Contrôle des props

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.

Remarque importante

À partir de maintenant, vous utiliserez systématiquement PropTypes pour contrôler tous les composants ayant des propriétés.

Remarque importante

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.

Travail à réaliser
  • Contraindre les props à l'aide de PropTypes.

Délégation de contenu

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 :

Travail à réaliser
  • Utiliser une props children pour définir le contenu du composant Card.
  • Mise à jour des PropTypes du composant Card.
  • Modifier en conséquence les appels au composants Card dans l'application.

Un second composant

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
  • et 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 :

Travail à réaliser
  • Création du composant Button.
  • Utilisation du composant Button dans le composant App.

Utilisation de polices d'icônes (icon fonts)

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.

Travail à réaliser
  • Installation des icônes Font Awesome.
  • Utilisation des icônes Font Awesome dans le composant Button.

Ajout d'interaction

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.

Travail à réaliser
  • Utilisation de la propriété onClick dans le composant Button.
  • Ajout d'une props onClick au composant Button.
  • Utilisation de la props onClick du composant Button dans le composant App.

Les données d'un composant

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  :

  • vous incrémenterez la valeur de la variable cpt de 1,
  • vous afficherez la valeur de la variable cpt dans la console de dévoppement.

et pour finir, vous ajouterez l'affichage de la valeur de la variable cpt dans le contenu de vos boutons.

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.

Travail à réaliser
  • Test de l'utilisation d'une variable cpt pour compter les clics des boutons.
  • Ajout d'un état cpt dans l'application.
  • Modification de cet état lors du clic de la souris sur les boutons.

Spécialisation de composant

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

  • intégrera une variable d'état cpt, qui contiendra la valeur du compteur,
  • supportera trois props :
    • 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.
  • proposera un rendu similaire à la maquette suivante :

Vous remplacerez les instances du composant Button de l'application dans le composant App.

Travail à réaliser
  • Création du composant Counter.
  • Utilisation du composant Counter dans le composant App.

Remontée d'information au parent

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.

Travail à réaliser
  • Ajout d'une props onChange au composant Counter.
  • Utilisation du hook useEffect pour invoquer la props onChange lors des modification de la variable d'état cpt.
  • Ajout d'une variable d'état counts dans le composant App.
  • Affichage du total des compteurs.
  • Modification de la variable d'état counts dans les fonctions de rappels associées à la props onChange des compteurs.

Retour sur la spécialisation - composition de composant

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 :

Travail à réaliser
  • Créer le composant d'ordre supérieur FoldableCard.
  • Ajouter et utiliser la props opened au composant FoldableCard.
  • Gérer l'affichage de la carte repliée à l'aide des classes CSS.
  • Gérer l'affichage conditionnel du contenu de la carte repliée.
  • Ajouter et utiliser la props className au composant Card.
  • Utiliser le composant FoldableCard dans l'application.

Ajout d'une icône

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.

Travail à réaliser
  • Utilisation des icônes Font Awesome dans le composant FoldableCard.

Modification de l'état de la carte repliable

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.

Travail à réaliser
  • Ajout d'un état isShown dans le composant FoldableCard.
  • Ajout d'une propriété onClick au composant Card.
  • Utilisation de la propriété onClick du composant Card dans le composant FoldableCard.
  • Modification de la variable d'état isShown lors du clic de la souris.

Réparation du compteur

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 :

  • recevant en paramètre un événement dont vous allez stopper la propagation,
  • et invoquant la props 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 :

  • en premier, si la remise à zero du compteur convient à votre application, vous remarquerez néanmoins que le total des compteurs n'est pas mis-à-jour lors de la disparition du compteur. Le compteur doit signaler que la valeur de son compteur passe à 0 lors de la suppression du composant.
  • en second, si vous souhaitez que les compteurs conserves leur valeur lorsqu'ils réapparaissent, ils doivent proposer une solution pour initialiser leur valeur.

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.

Travail à réaliser
  • Corriger la propagation d'événement dans le composant Button.
  • Étudier et comprendre l'utilisation du retour du hook useEffect.
  • Initialiser la valeur des compteurs à l'aide des valeurs de la variable d'état counts du composant App.

Restructuration du code et hook personnalisé

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
  • et 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.

Travail à réaliser
  • Création du hook personnalisé useShowable dans le sous-répertoire « src/hooks ».
  • Utilisation du hook dans le composant FoldableCard.

Gestions des cartes

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.

Travail à réaliser
  • Création du composant Cards.

Gestion des tableaux

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.

Travail à réaliser
  • Ajout de la propriété cardsData au composant Cards.
  • Mise à jour des PropTypes du composant Cards.
  • Rendu de FoldableCard à partir de la propriété cardsData.

Unicité de l'ouverture d'une carte repliable

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.

Travail à réaliser
  • Ajout d'une variable d'état openedId dans le composant Cards, stockant l'id de la carte dépliée.
  • Ajout d'une propriété onToggleOpened au composant FoldableCard.
  • Dans le composant Cards utilisation de la propriété onToggleOpened pour mettre à jour la variable d'état openedId.
  • Utilisation du hook useEffect pour mettre à jour la variable d'état isShown.
A. Jonquet DUT-INFO/REIMS