Aller au contenu principal

Composants en React

Utiliser des composants existant. Exemple de Bootstrap

Les composants permettent de découper l'interface utilisateur en éléments indépendants et réutilisables, permettant ainsi de considérer chaque élément de manière isolée. Cette page fournit une introduction au concept de composants. Vous trouverez une référence détaillée de l’API des composants ici. Conceptuellement, les composants sont comme des fonctions JavaScript. Ils acceptent des entrées quelconques (appelées « props ») et renvoient des éléments React décrivant ce qui doit apparaître à l’écran.

Utiliser des composants permet de :

  • Faciliter la mise en place d'interfaces utilisateur,
  • Interagir avec des données provenant d'API,
  • Apprendre les meilleures pratiques pour intégrer des bibliothèques dans un projet React.

Il faut commencer par installer le package Bootstrap

npm install react-bootstrap

Ce site référence tous les composants proposés par bootstrap.

Dans l'exemple ci-dessous j'ai utilisé :

  • les balises permettant de structurer ma page pour insérer mes composants dans une grille (App.tsx)
  • le composant Card (une image avec un texte en dessous)
  • le composant Modal
  • Le composant Accordeon

composants

Travail à faire
  • Lisez ce code et les commentaires.
  • Ajoutez chaque fichier à votre projet.
  • Complétez le fichier App.tsx afin d'intégrer les composants
  • Soyez un peu curieux et ajoutez d'autres composants issus de ce site

Composant Card

CardGrid.tsx
import React from 'react';
import { Card, Row, Col } from 'react-bootstrap';

const CardGrid: React.FC = () => {
//TSX donc on fait l'effort de déclarer des types (ou interfaces)
ttype CardItem = {
title: string;
text: string;
img: string;
}
//constitution des données (en déclarant le un tableau de CardItem)
const cards: CardItem[] = [
{ title: 'Card 1', text: 'This is card 1.', img: 'https://via.placeholder.com/150' },
{ title: 'Card 2', text: 'This is card 2.', img: 'https://via.placeholder.com/150' },
{ title: 'Card 3', text: 'This is card 3.', img: 'https://via.placeholder.com/150' },
];

return (
<Row>
{/*Comme toujours, react attend un tableau pour faire le rendu*/}
{/*Il faut donc itérer sur cards avec .map*/}
{cards.map((card, idx) => (
<Col md={4} key={idx} className="mb-4">
{/*Utilisation du composant card créé par boostrap*/}
{/*Il faut lire la documentation pour savoir comment l'utiliser*/}
<Card>
<Card.Img variant="top" src={card.img} />
<Card.Body>
<Card.Title>{card.title}</Card.Title>
<Card.Text>{card.text}</Card.Text>
</Card.Body>
</Card>
</Col>
))}
</Row>
);
};

export default CardGrid;

Composant Modal

ModalExample.tsx
import React, { useState } from 'react';
import { Modal, Button } from 'react-bootstrap';

const ModalExample: React.FC = () => {
// Définition du state pour gérer l'affichage du modal
const [show, setShow] = useState<boolean>(false);

// Fonctions pour ouvrir et fermer le modal
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);

return (
<>
{/* Bouton pour ouvrir le modal */}
<Button variant="primary" onClick={handleShow}>
Open Modal
</Button>

{/* le state show est modifié à chaque clic sur le boutton */}
{/* l'attribut show du composant Modal permet d'indiquer si la modale s'affiche ou non. */}
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Modal Title</Modal.Title>
</Modal.Header>
<Modal.Body>
This is a demo modal with React-Bootstrap. You can add more content here.
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
};

export default ModalExample;

Composant Accordéon

AccordionExample.tsx
import React from 'react';
import { Accordion } from 'react-bootstrap';

const AccordionExample: React.FC = () => {
return (
<Accordion defaultActiveKey="0">
<Accordion.Item eventKey="0">
<Accordion.Header>Item 1</Accordion.Header>
<Accordion.Body>
This is the content of the first item.
</Accordion.Body>
</Accordion.Item>
<Accordion.Item eventKey="1">
<Accordion.Header>Item 2</Accordion.Header>
<Accordion.Body>
This is the content of the second item.
</Accordion.Body>
</Accordion.Item>
</Accordion>
);
};

export default AccordionExample;

Intégrer les composants

remarque

Il ne reste plus qu'à insérer tous ces composants dans App.tsx. Compléter le code ci-dessous. Pour intégrer un composant il suffit de l'importer puis de l'ajouter comme une balise html classique : <nomDeMonComposant />

App.tsx
import React from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import CardGrid from './CardGrid';
import ModalExample from './ModalExample';
import AccordionExample from './AccordionExample';

const App: React.FC = () => {
return (
<Container fluid className="min-vh-100 d-flex flex-column justify-content-center align-items-center">
<h1 className="text-center my-4">React-Bootstrap Demo</h1>
<Row className="mb-4 justify-content-center">
<Col lg={6} md={12} className="text-center">
{/*insérer ici le composant CardGrid*/}
</Col>
<Col lg={6} md={12} className="text-center">
{/*insérer ici le composant modal*/}
</Col>
</Row>
<Row className="mb-4 justify-content-center">
<Col lg={6} md={12} className="text-center">
{/*insérer ici le composant Accordéon*/}
</Col>
</Row>

</Container>
);
};

export default App;

Créer ses propres composants

Les Props

En React, les props (propriétés) sont utilisées pour transmettre des données à un composant React. Les props sont essentiellement des arguments pour les composants React. Elles permettent de configurer ou personnaliser un composant en lui passant des données spécifiques.

Voici quelques points clés concernant les props en React :

  1. Passage des Props : Les props sont passées aux composants de parent à enfant. Un composant parent peut transmettre des valeurs à ses composants enfants via les props.

  2. Composants Fonctionnels : Dans un composant fonctionnel, les props sont passées en tant qu'argument de la fonction.

Appel à son propre composant en lui passant des données (props)

App.tsx
import React from 'react';
import MonComposant from './MonComposant';

const App: React.FC = () => {
return (
<div>
<MonComposant message="Bienvenue à React avec TypeScript !" count={5} />
<MonComposant message="Autre message" count={-1} />
</div>
);
};

export default Parent;
  1. Propriétés Immuables : Les props sont immuables, ce qui signifie qu'une fois qu'elles sont définies pour un composant, elles ne peuvent pas être modifiées par ce composant lui-même. Elles sont destinées à être lues uniquement.

  2. Utilisation des Props : Vous pouvez utiliser les valeurs des props pour personnaliser le rendu d'un composant. Par exemple :

Moncomposant.tsx
import React from 'react';

// Définition d'un type pour les props
type MonComposantProps = {
message: string;
count: number;
}

// Composant fonctionnel avec des props typées
const MonComposant: React.FC<MonComposantProps> = (props) => {
// Utilisation des props dans le composant
return (
<div>
<p>{props.message}</p>
<p>Count vaut : {props.count}</p>
</div>
);
};

export default MonComposant;

Les props sont un mécanisme fondamental dans React pour rendre les composants dynamiques et réutilisables en leur permettant de prendre différentes données en fonction de l'endroit où ils sont utilisés.

Voici un autre exemple permettant de prendre un tableau d'objets et de les afficher sous forme de liste.

Moncomposant.tsx
import React from 'react';

// Déclaration du type fruit
type Fruit = {
nom: string;
couleur: string;
}

// Déclaration du type des props attendues
type ListeProps = {
fruits: Fruit[]; // Tableau d'objets Fruit
}

const MaListe: React.FC<ListeProps> = ({ fruits }) => {
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>
{fruit.nom} - {fruit.couleur}
</li>
))}
</ul>
);
};

export default MaListe;

App.tsx
const fruits: { nom: string; couleur: string }[] = [
{ nom: "Pomme", couleur: "Rouge" },
{ nom: "Banane", couleur: "Jaune" },
{ nom: "Orange", couleur: "Orange" },
{ nom: "Fraise", couleur: "Rouge" },
];

return (
<MaListe fruits={fruits} />
);
danger

La solution proposée ci-dessus a le défaut de définir deux fois le type fruit.

Il est préférable de définir le type une seule fois pour éviter la duplication. Pour cela, vous pouvez déplacer la définition du type Fruit dans un fichier séparé et l'importer dans votre composant ainsi que dans votre App.tsx. Cela rendra votre code plus maintenable et centralisé.

types.ts
export type Fruit = {
nom: string;
couleur: string;
}

Dans les fichiers où vous en avez besoin il suffit de l'importer


import { Fruit } from './types';

const App: React.FC = () => {

const fruits: Fruit[] = [
{ nom: "Pomme", couleur: "Rouge" },
{ nom: "Banane", couleur: "Jaune" },
{ nom: "Orange", couleur: "Orange" },
{ nom: "Fraise", couleur: "Rouge" },
];

return (
<div>
<h1>Liste des fruits</h1>
<Liste fruits={fruits} />
</div>
);
};

export default App;

Il faut faire la même chose dans Liste.tsx

type ListeProps = {
import React from 'react';
import { Fruit } from './types'; // Import du type

// Déclaration du type des props attendues
type ListeProps = {
fruits: Fruit[]; // Tableau d'objets Fruit
}

const MaListe: React.FC<ListeProps> = ({ fruits }) => {
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>
{fruit.nom} - {fruit.couleur}
</li>
))}
</ul>
);
};

export default MaListe;
}

C'est une bonne habitude à prendre. Cela rend votre code plus propre, plus modulaire et plus facile à maintenir à long terme.

Gestion des évènements avec React

Comprendre la mécanique de typage des évènements

En React, les événements sont nommés en utilisant la syntaxe camelCase plutôt qu'en minuscules, comme c'est généralement le cas dans le DOM. Par exemple, onClick plutôt que onclick. De plus, vous passez une fonction en tant que gestionnaire d'événements au lieu d'une chaîne. N'oubliez pas que vous faites du tsx. Il faut donc penser à typer tout ce qui peut l'être. Ce n'est pas simple de connaitre par coeur tous les types existants. Il y en a même plusieurs possibles. Par exmeple dans l'exemple ci dessous, event pourrait être un React.MouseEvent ou être plus spécifique en disant que c'est un event est de type React.MouseEvent<HTMLButtonElement>. On indique dans ce dernier cas que l'évènement ne peut se déclencher que s'il est déclenché par un élément HTMLButtonElement. Cela permet à TypeScript de connaître précisément les propriétés disponibles sur event.currentTarget ou event.target. Par exemple, TypeScript saura que c'est un bouton HTML, ce qui permet d'accéder à ses propriétés spécifiques.

Il existe des cheat sheets sur internet pour vous aider à trouver les bons types [1] ou [2] ou [3]

import React from 'react';

const ComposantClic: React.FC = () => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
alert(`Button cliqué en position ${event.clientX}/${event.clientY}`);
}
return (
<button onClick={handleClick}>
Cliquez-moi
</button>
);
}

export default ComposantClic;
Bien chosir le type

Dans l'exemple précédent, si vous aviez choisi le type plus générique React.MouseEven, alors l'évènenent peut être déclenché par n'importe quel élément html (par exemple div)

const handleClick = (event: React.MouseEvent) => {
console.log(event.clientX, event.clientY); // Coordonnées du clic
};

Si vous essayez d'accéder à des propriétés spécifiques à un type d'élément (par exemple, event.currentTarget.value pour un <button>), TypeScript ne pourra pas vous garantir la validité et pourrait produire une erreur.

Passage de Fonction de Rappel de Parent à Enfant en React

En React, le passage de données entre composants peut s'effectuer de parent à enfant à l'aide de propriétés (props). Cela inclut la possibilité de passer des fonctions de rappel du composant parent à ses composants enfants. Ce mécanisme permet au parent de déléguer des actions spécifiques aux enfants, tout en maintenant une structure de composant modulaire.

Considérons l'exemple suivant où un composant parent intègre deux composants enfants :

  • un composant avec un input
  • un autre comopsant qui affiche une valeur qui lui est passée via sa props

Le code ci-dessous permet

  • de capturer le changement de valeur dans le champ input (dans PremierFils.tsx) puis de transmettre au composant père (ComposantParent.tsx) cette valeur via la méthode callBack passée en paramètre
  • le composant père exécute sa callBack qui met à jour le state de la variable value
  • ce changement d'état fait que React réaffiche le composant parent qui du coup réaffiche le composant SecondFils (dans SecondFils.tsx)avec la nouvelle valeur de value
ComposantParent.tsx
import React,{useState} from "react"
import PremierFils from "./PremierFils";
import SecondFils from "./SecondFils";

const ComposantParent: React.FC = () => {
const [value, setValue] = useState<string>("");

return (
<div>
<h1>Exemple Parent-Enfant</h1>
{/* Enfant qui modifie la valeur */}
<PremierFils onUpdate={setValue} />
{/* Enfant qui affiche la valeur */}
<SecondFils value={value} />
</div>
);
};

export default ComposantParent;
PremierFils.tsx
import React from "react"

const PremierFils: React.FC<{ onUpdate: (newValue: string) => void }> = ({ onUpdate }) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onUpdate(event.target.value);
};

return (
<div>
<input type="text" placeholder="Entrez une nouvelle valeur" onChange={handleChange} />
</div>
);
};

export default PremierFils;
SecondFils.tsx
import React from "react";

// Composant Enfant pour afficher la valeur
const SecondFils: React.FC<{ value: string }> = ({ value }) => {
return <div>Valeur actuelle : {value}</div>;
};

export default SecondFils;

Cela offre une approche déclarative pour gérer les actions dans le composant parent, tout en permettant aux enfants d'être réutilisables et indépendants de leur contexte d'utilisation. Ce modèle de communication descendante est au cœur de la philosophie de React, où les données et le comportement descendent de parent à enfant de manière prévisible.

attention au prop drilling

Attention, ce mécanisme est très courant, mais il ne faut pas en abuser en termes de chaines de CallBack. Si vous imbriqués plusieurs niveaux : le fils envoie à son père la props via sa CallBack qui l'envoie à son propre père, qui l'envoie à son propre père pour finalement faire redescendre à un autre de ses fils ... Alors plusieurs problèmes apparaissent :

  • Complexité croissante : Lorsque vous avez plusieurs niveaux de composants entre la source de données (le parent) et leur consommateur (l'enfant), chaque niveau doit transmettre manuellement les props, même s'il ne les utilise pas directement.
  • Couplage fort : Les composants enfants deviennent fortement dépendants de leurs parents, ce qui réduit leur réutilisabilité et rend le code moins modulaire.
  • Maintenance difficile : Si vous modifiez la structure de vos composants ou ajoutez de nouveaux niveaux, vous devez également ajuster la chaîne de props, ce qui peut introduire des erreurs.

Nous verrons dans la section hook qu'on peut utiliser useContexte pour éviter d'avoir à transmettre des propssur trop de niveaux.

Exercice

La vidéo ci-dessous illustre le résultat attendu. On entre une valeur dans l'un des deux champs input. Cela affiche la température dans l'autre unité. En plus cela affiche "l'eau boue" si la température est supérieure ou égale à 100. Cet exercice est librement inspiré du tutoriel de la documentation officielle de REACT.

Architecture du composant

Le code est structuré de la manière suivante :

  • Créez un répertoire convertTemp vous y mettrez tous les fichiers décrits si dessous. Commencez à prendre de bonnes habitudes, certains composants nécessitent plusieurs fichiers (des sous composants qui lui sont liés), donc créez un répertoire dédié au composant final.
  • créez le fichier types.ts et déclarez export type Scale = "c" | "f";
  • créez le fichier TemperatureInput.tsx. Il contiendra le code d'un composant permettant de saisir un nombre et de l'envoyer à son composant père dès que la valeur change
  • créez le fichier BoilingVerdict.tsx. Il contiendra le code d'un composant affichant "The water would boil." si la valeur passée (via sa props) est >= 100, sinon il affichera "The water would not boil."
  • créez le fichier Calculator.tsx. C'est le code du composant "père". Il contient :
    • la logique métier pour convertir d'une unité à une autre
    • l'intégration des deux input (deux fois le composant TemperatureInput)
    • l'intégration du composant BoilingVerdict.

Complétez les codes ci-dessous en suivant les indications

import React from "react";

type BoilingVerdictProps = {
celsius: number;
}

const BoilingVerdict: React.FC</* à vous de compléter*/> = ({ celsius }) => {
return (
<p>
{/* à vous de compléter*/ >= 100
? /* à vous de compléter*/
: /* à vous de compléter*/}
</p>
);
};

export default BoilingVerdict;
import React from "react";
import { Scale } from "./types";

//j'ai choisi de faire un record
// pour pouvoir accéder simplement
// à une valeur en fonction d'une clé.
const scaleNames: Record<Scale, string> = {
c: "Celsius",
f: "Fahrenheit",
};

//définition du type de l'ojet passé en props
type TemperatureInputProps = {
scale:/* à vous de compléter*/;
temperature: /* à vous de compléter*/;
//la méthode callBAck passée par le composant père
onTemperatureChange: (temperature: string) => void;
}


const TemperatureInput: React.FC</* à vous de compléter*/> = ({
//un peu de desctructuration (cf cours de l'année dernière)
scale,
temperature,
onTemperatureChange,
}) => {
const handleChange = (e: /* à vous de compléter*/) => {
onTemperatureChange(/* à vous de compléter*/);
};

return (
<fieldset>
<legend>Enter temperature in {/* à vous de compléter*/}:</legend>
<input value={/* à vous de compléter*/} onChange={/* à vous de compléter*/} />
</fieldset>
);
};

export default TemperatureInput;
import React, { useState } from "react";
import TemperatureInput from "./TemperatureInput";
import BoilingVerdict from "./BoilingVerdict";
import { Scale } from "./types";

// Fonctions utilitaires pour les conversions
const toCelsius = (fahrenheit: number): number => ((fahrenheit - 32) * 5) / 9;
const toFahrenheit = (celsius: number): number => (celsius * 9) / 5 + 32;

const tryConvert = (
temperature: string,
//fonction qui prend un number en paramètre et retourne un number
convert: (temp: number) => number
): string => {
//corps de la fonction TryConvert
// cette manière de coupler ce code à tryConvert permet
// d'appliquer n'importe quelle logique de conversion.
//on ne sait pas à l'avance si elle va convertir Fahrenheit en Celsius ou Celsius en Fahrenheit.
//l'appel à convert sera soit toCelsius soit toFahrenheit
const value = parseFloat(temperature);
if (Number.isNaN(value)) return "";
return (Math.round(convert(value) * 100) / 100).toString();
};

const Calculator: React.FC = () => {
//encore les variables d'états du composant
const [temperature, setTemperature] = useState<string>("");
const [scale, setScale] = useState<Scale>("c");

const handleCelsiusChange = (temperature: string) => {
setScale("c");
setTemperature(temperature);
};

const handleFahrenheitChange = (temperature: string) => {
/* à vous de compléter*/
/* à vous de compléter*/
};

//on caclule les deux valeurs
const celsius =
scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit =
scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

//jsx
return (
<div>
<{/* à vous de compléter */}
scale="c"
temperature={/* à vous de compléter*/}
onTemperatureChange={/* à vous de compléter*/}
/>
<{/* à vous de compléter */}
scale="f"
temperature={/* à vous de compléter*/}
onTemperatureChange={/* à vous de compléter*/}
/>
<{/* à vous de compléter */} celsius={parseFloat(/* à vous de compléter*/) || 0} />
</div>
);
};

export default Calculator;