React

React, créé par Facebook, est une bibliothèque JavaScript populaire pour construire des interfaces utilisateur interactives et modulaires, grâce à l'utilisation de composants réutilisables.

Informations

Date de publication :

Date de modification :

Catégories : react, javascript, ajax, api, html, css, jsx

Auteur : Photo de profil de l'auteur de la documentation meezyr

Bannière documentation React

React est une librairie (également appelée bibliothèque) JavaScript permettant de créer des interfaces utilisateurs pour des applications. Il s’agit aujourd’hui du framework JavaScript le plus utilisé au monde. React est utilisé par Netflix, Yahoo, Airbnb, Sony, Atlassian, Facebook, Instagram, WhatsApp, Microsoft, Paypal et tant d’autres entreprises. React utilise un DOM virtuel pour obtenir des performances très supérieures à du JavaScript natif. En effet, au lieu de modifier directement le DOM, qui est l’arbre des éléments HTML, ce qui est coûteux en performance, React va calculer à l’aide d’un DOM virtuel tous les changements optimaux à effectuer sur le DOM avant de les effectuer. Avec React au lieu d’utiliser du HTML, nous utilisons du JSX. Ce n’est pas obligatoire mais fortement recommandé. Le JSX permet d’écrire du code HTML dans du code JavaScript. Ce code sera ensuite transpilé en JavaScript. Grâce à React Native vous pouvez facilement porter votre application React sur mobile. React est un framework permettant de construire des Single Page Applications (SPA). Une SPA est une application qui fonctionne dans un navigateur sans que l’utilisateur n’ait besoin de recharger la page. Il vous faut donc le gestionnaire de dépendances pour les environnements JavaScript : npm. Il est conçu pour simplifier l’installation, la mise à jour et la désinstallation des librairies utilisées dans votre application. Npm est inclus dans Node.js. Webpack permet de les organiser de manière optimisés pour les navigateurs, il est utilisé par react et create-react-app gère toute la configuration pour utiliser Webpack dans un projet React. Pour en savoir plus React, consultez la documentation officielle.

1. Les bases

1.1 Installation

Pour créer une nouvelle application React, utilisez npx pour exécuter la dernière version de create-react-app, grâce à la commande suivante :

npx create-react-app my-app

Pour en savoir plus sur l'installation de React, consultez la documentation.

1.2 Lancement

La commande npm start permet de lancer l'application en version de développement et ainsi retrouver l'application à cette adresse : http://localhost:3000/.

La commande qui permet gérérer l'application pour la version de production est la suivante :

npm run build

1.3 Organisation

Dans React, il y a de multiples dossier qui ont tous un rôle, en voici une liste :

  • Le dossier public comporte tous les éléments qui ne sont pas optimisés par Webpack.
  • Le fichier public/manifest.json est nécessaire pour rendre votre application progressive (PWA ou progressive web app).
  • Le fichier public/index.html est la seule page HTML de votre application. Il contient les imports notamment du favicon de votre application, du manifest.json, et bien sûr, des fichiers de votre application qui seront injectés par Webpack lors du build.
  • Le dossier src est la source de votre application. Il contiendra toute l’application que vous développerez avec React.
  • Le fichier src/index.js est le premier fichier JavaScript à s’exécuter.
  • Le fichier src/index.css contient les styles globaux de l'application qui seront disponibles dans toute l'application.
  • Le fichier src/App.test.js est le test unitaire du fichier App.js.
  • Le fichier src/App.js contient la partie logique du composant racine de l’application.
  • Le fichier src/App.css contient le style du composant.
  • Le fichier src/reportWebVitals.js permet de paramétrer une librairie développée par Google, Web-Vitals, qui permet de mesurer en temps réel les performances de votre application en production. Cela permet de s'assurer que les utilisateurs aient une bonne expérience lorsqu'ils utilisent l'application.
  • Le fichier src/setupTests.js permet de gérer la configuration des tests.

La méthode ReactDOM.createRoot(DOMNode) permet de définir le nœud racine du DOM sur lequel toute l'application React sera rendue. Elle permet également d'activer le mode concurrent disponible depuis la version 18.

La méthode render(reactNode) permet de rendre l’élément React passé en premier argument sur l'élément passé à ReactDOM.createRoot(). Cela permet donc ici d'afficher notre composant App, appelé composant racine, dans l’élément HTML qui a pour id root. Cet élément se trouve dans le body de index.html.

1.4 Configuration

La balise <App /> permet d'indiquer où placer le composant React App, celui-ci est situé dans le fichier App.js.

L'instruction React.StrictMode permet de détecter les problèmes potentiels d’une application. Il active des vérifications et avertissements supplémentaires pour toute l'application React car nous lui passons l'élément racine, tout en haut de l'arbre des composants.

1.5 React dans HTML natif

Il est possible d'utiliser React pour une petite partie d'une application, dans VS Code, il faut utiliser Live Server. Par exemple :

<!DOCTYPE html>
<html lang="en">
    <head>
        ...
        <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
        <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    </head>
    <body>
        <div id="app"></div>
        <script type="text/babel">
            const root = ReactDOM.createRoot(document.getElementById('app'));
            const searchBar = <input placeholder="rechercher" />;
            root.render(searchBar);
        </script>
    </body>
</html>

2. JSX

Pour en savoir plus sur le JSX de React, consultez la documentation.

2.1 Éléments React

Les éléments React sont des objets qui décrivent ce que vous voulez afficher à l’écran. React utilise ces objets pour construire le DOM et le mettre à jour. Pour créer un élément React il suffit d’utiliser la méthode prévue :

React.createElement(
    type,
    [props],
    [...children]
)

Le langage JSX est une extension syntaxique à JavaScript qui permet d’utiliser du HTML amélioré dans du JavaScript. Par exemple :

import React from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
const element = React.createElement('h1', { className: 'my-class' }, 'Bonjour !');

// Ou

const element = <h1 className='my-class'>Bonjour !</h1>;

root.render(element);

Les éléments React sont immuables (immutables), cela veut dire qu’une fois défini ils ne peuvent être modifiés lors de l’exécution. Par exemple :

const element = (
    <div>
        <h2>Il est exactement {new Date().toLocaleTimeString()} lors de l'exécution.</h2>
        // L’expression est évaluée et affichée et n’est pas mise à jour par défaut
    </div>
);

Le seul moyen de mettre à jour un élément est d’en créer un nouveau. Il sera réévalué en un nouvel objet JavaScript avec une référence distincte. Dans l’exemple, nous isolons l’élément dans une const qui a une portée de bloc pour que chaque exécutions de l’intervalle crée un tout nouvel élément JavaScript sans aucune référence au précédent. Dans ce cas, le DOM est mis à jour mais pas entièrement ! Rappelez-vous que React utilise un DOM virtuel et il va utiliser des algorithmes de comparaison très performants pour comparer le DOM virtuel avant et après la modification pour ne mettre à jour que l’élément HTML qui a été modifié sur le DOM :

setInterval(() => {
    const element = (
        <div>
            <h1>Bonjour de Dyma</h1>
            <h2>Il est exactement {new Date().toLocaleTimeString()} pour ce nouvel élément unique.</h2>
        </div>
    );

    root.render(element);
}, 1000);

2.2 Expressions

Avec JSX vous pouvez utiliser des expressions JavaScript directement en utilisant des accolades, comme dans l'exemple suivant :

const prenom = 'Jean';
const element = <h1>Bonjour, {prenom}</h1>;

Il est possible d’utiliser toute expression JavaScript qui peut être convertie en chaîne de caractères. Par exemple :

const direBonjour = () => 'Bonjour !';
const obj = {
    prenom: 'Jean',
};
const element = (
    <div>
        <p>Vous pouvez utiliser des nombres : { 2 + 3 }</p>
        <p>Vous pouvez utiliser des fonctions : { direBonjour() }</p>
        <p>Vous pouvez utiliser des propriétés d'objet : { obj.prenom }</p>
        <p>Vous NE POUVEZ PAS utiliser des objet directement.</p>
    </div>
);

Du code JSX est une expression valide qui peut être utilisée également. L’expression sera de toute façon transpilée en JavaScript, par exemple :

const monExpressionJx = () => {
    if (true) {
            return <div>Il est possible d'utiliser une expression JSX !</div>;
    } else {
            return <div>Il est possible d'utiliser une expression JSX !</div>;
    }
};
const element = monExpressionJx();

2.3 Attributs

Vous pouvez définir des attributs en utilisant du JSX par exemple assigner la valeur de l’attribut src dans une image :

const lien = "https://lemagduchat.ouest-france.fr/images/dossiers/2023-06/mini/chat-cinema-061232-650-400.jpg";
const element = <img src={lien}/>; // Si l'élément est vide vous pouvez utiliser la noation raccourcie /> : <img />

2.4 Fragments

En React, il est obligatoire que l'élément rendu ait un élément parent unique. C'est-à-dire qu'il faut qu'il y ait un élément contenant s'il y a plusieurs éléments enfants. Comme il est courant pour un composant de renvoyer plusieurs éléments. Les fragments nous permettent de grouper une liste d’enfants sans ajouter de nœud supplémentaire au DOM. Par exemple :

const element = (
    <>
        <div>
            <h1>Bonjour de Dyma</h1>
        </div>
        <p>Un paragraphe</p>
    </>
);

3. Composants

Un composant est une brique réutilisable et isolée. Un composant React est une fonction qui peut optionnellement recevoir en paramètres des propriétés et qui retourne un élément React. Cet élément React peut par exemple être du markup JSX. Le nom d'un composant doit commencer par une lettre majuscule. Par exemple :

function MonBouton() {
    return <button>Cliquez moi</button>;
}

root.render(<MonBouton />); // Ou <MonBouton></MonBouton>

Les applications React sont découpées en un très grand nombre de composants qui sont répartis logiquement dans des fichiers. Pour exporter un composant depuis un fichier, vous pouvez l'exporter par défaut. Créez un fichier en UpperCamelCase avec le nom du composant par exemple MonSuperComposant et utilisez l'extension .js ou .jsx. Par exemple :

export default function MonSuperComposant() {
    return ( <h1>Bienvenue !</h1> );
}

Pour en savoir plus sur les composants de React, consultez la documentation.

3.1 Import et Export

Pour importer un composant qui utilise un export par défaut dans un autre fichier, on peut utiliser :

import MonSuperComposant from './MonSuperComposant';

Vous pouvez également exporté un composant de manière nommé, sans utilisation de default :

export function MonSuperComposant() {
 return ( <h1>Bienvenue !</h1> );
}

Pour importer un export nommé dans un autre fichier, dans ce cas il faut utiliser des accolades lors de l'import et vous devez obligatoirement utiliser le même nom que le nom de la fonction exportée.  On peut utiliser :

import { MonSuperComposant } from './MonSuperComposant';

Avec React, le plus souvent on utilise un export par défaut lorsqu'un fichier exporte un seul composant et on utilise les exports nommés lorsqu'un fichier exporte plusieurs composants et / ou valeurs. Mais on peut utiliser les deux en même temps :

import App, { MonSuperComposant } from './MonSuperComposant';

3.2 Props

Nous avons vu que l’objectif des composants React est d’être réutilisable. Pour que cela soit possible il faut pouvoir leur passer des propriétés afin qu’ils puissent les utiliser pour retourner des éléments JSX différents en fonction de celles-ci. Ces propriétés s’appellent les props. Il s’agit du moyen de React pour rendre les composants dynamiques en leur passant des données, d’un composant parent vers un composant enfant. Le flux de données est unidirectionnel comme nous allons le voir, c’est à dire qu’il va toujours dans le sens composant parent vers composant enfant. Un composant parent est un composant qui contient un ou plusieurs composants appelé(s) enfant(s). Par exemple :

<Composant prenom='Paul'/>

Les props ne sont pas modifiables dans les composants enfants, elles sont immuables ou immutables. Pour passer des props qui ne sont pas des chaînes de caractères il faut utiliser des accolades simples :

<Composant name="Jean" age={12} />

Pour passer des props qui sont des objets il faut utiliser des accolades doubles :

<Composant name="Jean" age={12} adresse={{ville: 'Paris', cp: '75017'}} />

Pour lire les props dans un composant enfant, vous pouvez utiliser l'objet props passé en argument :

export function Composant(props) {
    return ( <h2>Hello {props.name}! Tu as {props.age} ans</h2> );
}

Mais le plus souvent on utilise la décomposition JavaScript pour plus de lisibilité et de concision :

export function Composant({name, age}) {
    return ( <h2>Hello {name}! Tu as {age} ans</h2>);
}

Vous pouvez également, grâce à cette syntaxe, assigner des valeurs par défaut :

export function Composant({name, age=20}) {
    return ( <h2>Hello {name}! Tu as {age} ans</h2>);
}

Parfois vous voudrez simplement passer l'ensemble des props plus bas dans l'arbre des composants, par exemple Parent > Enfant > PetitEnfant. C'est-ce qu'on appelle le passage de props (props forwarding) et on utilise pour cela l'opérateur JavaScript spread (...). Par exemple :

function Enfant(props) {
    return (
        <div className="container">
            <PetitEnfant { ...props } />
        </div>
    );
}

Il est possible d'imbriquer du JSX à l'intérieur des balises d'un composant, il est possible de passer toute expression JSX, y compris un composant, comme vous le feriez avec du HTML :

<Composant>
    <button>Cliquez</button>
    <AutreComposant />
</Composant>

Dans ce cas, le composant pourra accéder et afficher au contenu JSX passé grâce à la propriété children :

export function Composant({ children }) {
    return (
        <>
            <h1>Hello world !</h1>
            { children }
        </>
    );
}

3.3 Suspence

Le composant natif Suspense permet d'afficher un élément React (par exemple un composant) en attendant que ses composants enfants aient terminé de charger. Suspense permet également de charger un composant de manière paresseuse (lazy-loading). Par exemple :

<Suspense fallback={<Chargement />}>
    <UnComposant />
</Suspense>

Le composant Suspense permet de charger de manière différée des composants (lazy-loading). Pour charger un composant de manière différée, il faut utiliser la fonction lazy. Ce code utilise l'import dynamique qui est une fonctionnalité JavaScript récente. Maintenant que le code du composant est chargé de manière différée, lorsqu'il est demandé par l'application, il faut obligatoirement spécifier quoi afficher pendant le chargement en utilisant le composant Suspense :

import { lazy } from 'react';

const UnComposant = lazy(() => import('./UnComposant.js'));

<Suspense fallback={<h1>Chargement...</h1>}>
    <h2>Titre</h2>
    <UnComposant />
</Suspense>

4. Style et Class

Avec JSX et React, il est obligatoire de passer un objet à l'attribut style. Nous avions vu qu'il est obligatoire d'utiliser les doubles accolades pour passer un objet comme props et c'est la même règle avec l'attribut style. Comme tout est transformé en objet JavaScript avec JSX, nous ne pouvons pas utiliser des tirets dans les noms de propriétés CSS. Il est obligatoire d'utiliser le camelCase. Par exemple :

<p style={{ backgroundColor: 'black', color: 'pink' }}></p>

Nous avons déjà vu qu'il fallait obligatoirement utiliser l'attribut className et non class pour ajouter une ou plusieurs classes à un élément JSX :

<span className="menu navigation-menu">Menu</span>

Pour en savoir plus sur les styles de React, consultez la documentation.

4.1 Import du style

Grâce à Webpack vous pouvez directement créer un fichier Composant.css puis l'importer dans le fichier JavaScript Composant.js de cette manière :

import './Composant.css';

En production, tout le CSS est mis dans un seul fichier par Webpack lorsque vous construisez l'application. Notez donc que le CSS n'est pas du tout isolé par défaut en procédant de cette manière : tout le style défini est global à toute l'application. Il est donc conseillé de n'utiliser cette méthode que pour le style global de l'application qui est importé dans le fichier index.js par :

import './index.css';

4.2 Module CSS

Un module CSS est un fichier dont toutes les classes et les animations sont isolées localement par défaut. Les modules CSS ne sont pas une spécification CSS ou des navigateurs, il s’agit d’une étape de build de certains builder comme Webpack qui vont automatiquement changer le nom des class pour permettre cette isolation. Il est obligatoire que les modules CSS aient comme nom : [nomDuModule].module.css, ou avec l’utilisation de Sass : [nomDuModule].module.scss. Par exemple dans Message.module.css :

.theme {
    color: green;
}
.message {
    composes: theme; // L’utilisation de composes permet de fusionner des classes entre elles grâce aux modules CSS.
    font-size: 20px;
}

Pour utiliser ce style dans des composants, il suffit de l’importer et de les utiliser de cette manière :

import styles from './Message.module.css'
// ...
<li className={styles.message}> {message} </li>

4.3 Module SASS

Grâce à Webpack la class sera transformé en quelque chose qui ressemble à : class="Message_message_tp5lz". Pour utiliser plusieurs classes simultanément sur un élément, il suffit d’utiliser les littéraux de chaîne de caractères permis par JavaScript :

<div className={`${styles.classe1} ${styles.classe2}`}>

La configuration de Webpack pour utiliser Sass est déjà disponible create-react-app. Pour cela, il vous suffit d'utiliser la commande suivante :

npm i -D sass

Vous n’avez plus qu’à nommer vos modules avec [nom].module.scss et le tour est joué. Vous bénéficiez de la puissance de Sass et de l’isolation permise par les modules CSS, c’est-à-dire de tout ce que vous avez besoin pour créer une application importante.

En Sass, on peut faire du responsive avec les @mixins de la manière suivante dans un fichier _mixins.scss :

@mixin lg {
    @media (min-width: 993px) and (max-width: 1200px) {
       @content;
    }
}

Puis, modifier le responsive d'une class dans un fichier avec les mixins :

@use './mixins' as mixin;
.br {
    @include mixin.lg {
       border: 2px solid red;
    }
}

5. Conditions et boucles

5.1 Rendu conditionnel

Le rendu conditionnel d’éléments est simplement le fait d’afficher ou non un ou plusieurs éléments si une ou plusieurs conditions sont remplies. Lorsque vous voulez afficher un élément si une condition est respectée et un autre élément si elle n’est pas respectée, vous pouvez utiliser l’opérateur ternaire conditionnel. Cet opérateur JavaScript natif s’utilise de la manière suivante :

condition ? expr1 : expr2 ;

// Ou

{isDone ? (
    <div>
       <p>{ text + ' ✔' }</p>
    </div>
) : (
    <p>{ text }</p>
)}

5.2 Opérateur logique

Vous connaissez &&, il s’agit de l’opérateur logique “et”. Par exemple :

expr1 && expr2;

// Ou

{ text } { isDone && '✔' }

Vous pouvez même assigner du markup sur plusieurs lignes grâce à l'utilisation des parenthèses ou alors l'utilisation de sous composants :

function Todo({ text, isDone }) {
    let content = text;

    if (isDone) {
        content = (
            <div>
                { text + " ✔" }
            </div>
        );
    }

    return (
        { content }
    );
}

5.3 Afficher des listes

La méthode JavaScript map() permet de créer un nouveau tableau en appliquant une fonction sur chaque élément du tableau sur lequel elle est utilisée, comme dans l'exemple suivant :

const arr = [1, 2, 3];
const mapArr = arr.map(el => el * 2); // [2, 4, 6]

Pour afficher des listes avec React, nous avons deux moyens. Le premier est d’assigner à une variable un nouveau tableau contenant les éléments à afficher :

const nombres = [1, 2, 3];
const elements = nombres.map(n => <li>{n}</li>);

function Composant() {
    return (
        <div>
            <ul>{elements}</ul>
        </div>
    );
}

Le second est directement d’utiliser map() dans l’expression JSX retournée :

const nombres = [1, 2, 3];

function Composant() {
    return (
        <ul>
            {nombres.map(n => (
                <li>{n * 2}</li>
            ))}
        </ul>
    );
}

Les clés permettent à React de savoir à quel élément d'un tableau un élément de liste correspond. C'est important si les éléments d'un tableau peuvent être modifiés, par exemple leur index (lors d'un tri), ou s'ils sont supprimés ou insérés. Par exemple :

function Articles({}) {
    const articles = [
        { title: 'Un titre 1', content: 'Contenu 1', id: 1 },
        // ...
    ];

    return (
        <div>
            {articles.map((article) => (
                <Article
                    key={article.id}
                    content={article.content}
                    title={article.title}
                />
            ))}
        </div>
    );
}

Pour pouvoir utiliser la prop, il faut utiliser la syntaxe <Fragment> et non la syntaxe raccourcie <> que nous avions vue :

{articles.map((article) => (
    <Fragment key={article.id}>
       <h2>{article.title}</h2>
       <p>{article.content}</p>
    </Fragment>
))}

6. Événements

6.1 Fonctionnement de React

Pour React, tous vos composants sont des fonctions pures. Cela signifie que vos composants doivent toujours retourner le même JSX s'ils reçoivent les mêmes entrées. Les entrées d'un composant en React proviennent des props, du contexte ou de l'état (state). Il est possible de modifier des variables et des objets qui sont créés par le composant pendant le rendu. On ne peut pas modifier une variable externe au composant.

Avant que les composants soient affichés sur l'écran, ils doivent être rendus par React. Les étapes de ce rendu sont les suivantes :

  • Déclenchement (triggering) du rendu
  • Rendu (rendering) du composant
  • Affichage (committing) sur le DOM

Il y a deux raisons pour laquelle un rendu est déclenché : le rendu initial et le rendu lorsque l'état d'un composant change. Le rendu initial est simplement déclenché par :

ReactDOM.render()

Après le déclenchement d'un rendu, React exécute le composant pour savoir quoi afficher sur l'écran. C'est ce qu'on appelle le rendering.

Après la phase de rendering, et que tous les composants dont l'état a été modifié et leurs enfants aient été appelés, React va modifier le DOM. Pour les rendus suivants, React va effectuer uniquement les modifications minimales nécessaires qui ont été calculées pendant la phase de rendu (rendering).

6.2 Gestionnaire d'événement

Le gestionnaire d'événement est une fonction qui est exécutée lorsqu'un événement précis se produit sur l'élément cible. Pour définir un gestionnaire d'événement il faut passer la fonction comme une prop avec comme nom, le nom d'un événement précédé par on. Par exemple, le clic sur un bouton :

export default function Button() {
    function handleClick() {
        alert('Clic !');
    }

    return (
        <button onClick={handleClick}>Cliquez</button>
    );
}

Par convention, les noms des gestionnaires d'événement doivent être précédés par handle et suivi du nom de l'événement. Ici par exemple, handleClick(). Un autre exemple serait onMouseEnter={handleMouseEnter}.

Il est possible de directement définir la fonction dans le JSX mais c'est très rare car cela nuit à la lisibilité du composant. Il faut que le corps du gestionnaire soit très court. Par exemple :

<button onClick={() => {
    alert('Clic !');
}}>

Attention, lorsque l'on débute avec React on a tendance à exécuter les gestionnaires d'événement au lieu de passer leurs références :

<button onClick={handleClick}>

Une erreur commune est d'exécuter directement un gestionnaire d'événement qui prend un ou plusieurs arguments. Pour cela on doit créer une fonction anonyme qui crée une fonction gestionnaire, elle sera exécutée lors du clic :

<button onClick={() => alert('CLIC !')}>

Tous les événements se propagent avec React sauf onScroll. Pour stopper la propagation, ce qui est assez courant, il suffit d'appeler la méthode stopPropagation() sur l'événement reçu par le gestionnaire d'événement :

function handleClick(e) {
    e.stopPropagation();
    console.log('CLIC', e);
}

Pour empêcher ce comportement par défaut, qui est très souvent ce que nous voulons avec une SPA, il suffit d'appeler la méthode preventDefault() sur l'événement reçu par le gestionnaire d'événement. Le plus connu est le fait de rafraîchir la page lorsqu'un événement submit est émis dans un formulaire. Par exemple :

export default function Composant() {
    return (
        <form onSubmit={e => {
            e.preventDefault();
            alert('Envoyé !');
        }}>
            <input />
            <button>Envoyer</button>
        </form>
    );
}

Comme les gestionnaires d'événement sont déclarés dans les composants, vous pouvez accéder aux props du composant.

Très souvent, vous voudrez qu'un composant parent passe au composant enfant un gestionnaire d'événement différent suivant le contexte, il suffit de passer au composant enfant le gestionnaire d'événement en prop. Notez que le nom de la prop est totalement libre. Ce qui importe c'est que l'attribut sur le composant enfant ait bien le nom de l'événement précédé par on, comme dans l'exemple suivant :

<Button onButtonClick={handleClick} />
// Dans un composant
export default function Button({ onButtonClick }) {
    return (
        <button onClick={onButtonClick}>Submit</button>
    );
}

Les composants ont souvent besoin de changer ce qu'ils affichent en fonction d'une interaction d'un utilisateur et de sauvegarder des données comme la valeur courante d'un champ, un panier etc. La mémoire d'un composant est appelé état (state) en React. L'état d'un composant ne peut pas être sauvegardé dans des variables locales car elles ne sont pas sauvegardées entre les rendus d'un composant et le changement de la valeur d'une variable locale ne déclenchera aucun nouveau rendu du composant. Pour cette raison, nous utilisons des variables spéciales appelées variable d'état et une fonction set associée qui permet de mettre à jour cette variable et de déclencher un nouveau rendu.

7. Hooks

Pour en savoir plus sur les hooks de React, consultez la documentation.

7.1 Setter

Les hooks sont des fonctions qui permettent d'interagir avec la gestion d’état local et de cycle de vie de React depuis des composants. Le premier hook que nous allons voir est le hook d'état useState() qui permet de gérer un état local du composant. Par exemple :

import { useState } from 'react';

const [state, setState] = useState(valeurInitiale);

La convention pour le nom du setter est d'utiliser le préfixe set suivi du nom de la variable en camelCase. Il est très important que les hooks soient déclarés au premier niveau du composant et ne soient pas imbriquées. Les hooks sont comme des photos, ils sont figé :

function App() {
    const [count, setCount] = useState(0);

    function handleClick() {
        setCount(count + 1);
        console.log(count); // Résultat au premier clic du bouton : 0
        // Au prochaine clic sur le bouton le résultat sera : 1
    }

    return (
        <div>
            <button onClick={handleClick}>Submit {count}</button>
        </div>
    );
}

Voici les recommandations de React pour structurer l'état des composants :

  • Grouper les états reliés : si vous mettez à jour systématiquement deux ou plus variables d'état en même temps, fusionnez les dans une seule variable d'état.
  • Éviter les contradictions dans l'état : si plusieurs variables d'état entrent en contradiction suivant l'état de l'application, modifiez l'état.
  • Éviter les redondances : si vous pouvez calculer une information en utilisant plusieurs props ou plusieurs variables d'état ne créez pas une variable d'état pour cette information.
  • Éviter les duplications dans l'état : si les mêmes informations sont à plusieurs endroits de l'état, par exemple imbriquées dans plusieurs objets, cela devient difficile à maintenir. 
  • Éviter les imbrications profondes : évitez au maximum de créer des états qui comportent trop de niveaux d'imbrication d'objets / tableaux. Cela devient difficile pour les mises à jour.

C'est la mise à jour de l'état avec une fonction setter qui déclenche un nouveau rendu, React va comparer le nouvel état du DOM virtuel avec l'ancien état du DOM virtuel et va calculer les modifications minimales à apporter sur le DOM du navigateur pour qu'il corresponde au nouvel état demandé. Par exemple :

<button onClick={() => {
    setCompteur(compteur + 1);
    setCompteur(compteur + 1);
    setCompteur(compteur + 1);
}}></button>
<h1>{compteur}</h1> // Réponse après clic x3 : 1

La raison est que la fonction setter ne défini la nouvelle valeur de la variable d'état que pour le prochain rendu. Retenez donc que la valeur d'une variable d'état ne change jamais pendant un rendu donné. 

React attend que tous les gestionnaires d'événements aient été exécutés avant de réaliser les mises à jour des variables d'état demandées. Ce fonctionnement est appelé le traitement par lots ou batching. Il est possible de mettre à jour plusieurs fois la même variable d'état juste avant le même rendu, même si c'est très rarement nécessaire, en passant une fonction à la fonction setter :

<button onClick={() => {
    setCompteur(n => n + 1);
    setCompteur(n => n + 1);
    setCompteur(n => n + 1);
}}></button>
<h1>{compteur}</h1> // Réponse après clic x3 : 3

Les fonctions de mise à jour doivent être pures et retourner la nouvelle valeur de la variable d'état. Ensuite, au début du prochain rendu, React va les exécuter une par une pour déterminer la nouvelle valeur de la variable d'état pour le nouveau rendu.

En JavaScript, comme les objets sont passés par référence et non par valeur, il ne faut jamais modifier un objet contenu dans une variable d'état directement. Il faut d'abord copier l'objet et ensuite modifier cette copie et mettre à jour la variable d'état en utilisant la fonction setter. Par exemple :

const [position, setPosition] = useState({ x: 0, y: 0 });

Il faut utiliser la syntaxe spread pour copier les propriétés de l'objet contenu dans la variable d'état, puis modifier les propriétés souhaitées :

setPersonne({
    ...personne,
    prenom: 'John'
});

Notez que si vous avez des objets avec des propriétés contenant des objets (y compris des tableaux ou des fonctions qui sont des objets) il faudra penser à copier ces propriétés imbriquées, en effet l'utilisation de l'opérateur spread effectue une copie superficielle des propriétés d'un objet :

setPersonne({
    ...personne,
    adresse: {
        ...person.adresse,
        ville: 'Reims'
    }
});

Il faut toujours traiter l'état local d'un composant comme en lecture seule et ne jamais le modifier sans passer par une fonction setter. Il faut toujours créer une copie d'un tableau avant de modifier cette copie et de l'utiliser avec la fonction setter pour mettre à jour l'état lors du prochain rendu.

7.2 Context

Pour créer un Context, on utilise un fichier pour le déclarer avec createContext() et on l'exporte. On passe la valeur que l'on souhaite partager en argument. Par convention le nom du fichier est suffixé par Context, par exemple ExempleContext.js :

import { createContext } from 'react';

export const ExempleContext = createContext(42);

Comme tous les hooks il est obligatoire d'utiliser useContext() au premier niveau d'un composant (ni imbriqué dans un bloc conditionnel, ni dans une fonction). Pour ensuite l'utiliser dans n'importe quel composant il suffit de l'importer et d'utiliser le hook useContext(), comme dans l'exemple suivant :

import { useContext } from 'react';
import { ExempleContext } from './ExempleContext.js';

export default function UnComposant() {
    const data = useContext(ExempleContext);
}

Pour fournir (provide) un Context il faut l’importer et utiliser le composant <NomContext.provider value={valeur}>. Cela permet de changer la valeur fournie pour tous les composants situés plus bas dans l'arbre :

import ExempleContext from './ExempleContext';
import UnComposantEnfant from './UnComposantEnfant';

export default function UnComposantParnet() {
    return (
        <ExempleContext.Provider value={100}>
            <UnComposantEnfant />
        </ExempleContext.Provider>
    );
}

Le Context n'est pas forcément une valeur statique vous pouvez très bien le lier à une valeur dynamique, par exemple un état :

const [valeur, setValeur] = useState(42);

return (
    <ExempleContext.Provider value={valeur}>
        <UnComposantEnfant />
    </ExempleContext.Provider>
);

En effet, avec les props il est facile de voir d'où elles viennent et où elles vont. Elles sont donc plus facilement lisibles et maintenables. Voici des exemples d'usages recommandés du Context par l'équipe React :

  • Les thèmes : si votre application a deux ou plusieurs thèmes (par exemple un mode sombre), vous pouvez utiliser un Context pour indiquer le mode choisi aux composants qui ont besoin d'être adapté suivant le mode.
  • L'utilisateur connecté : de nombreux composants partout dans l'application ont souvent besoin d'avoir des informations sur l'utilisateur connecté. Cela permet de rendre disponible ces informations partout dans l'application.
  • Routing : la plupart des solutions de React pour le routing utilise en fait le Context. Nous verrons cela dans la suite de la formation.
  • Gérer l'état global : nous verrons que dans les applications importantes il peut être efficace de gérer l'état en combinant Context et le hook useReducer().

7.3 Reducer 

Pour ces composants, vous pouvez utiliser un hook spécifique qui permet de gérer toutes les mises à jour de l'état du composant dans une fonction unique, extérieure au composant, appelée reducer. Le hook useReducer() vient en remplacement de plusieurs hooks useState() dans un composant, par exemple :

const [state, dispatch] = useReducer(reducer, initialState);
  • state : état courant contenant un objet avec des propriétés. Vous pouvez le voir comme la fusion de tous les états locaux des useState().
  • dispatch : fonction permettant d'envoyer une action au reducer.
  • reducer : fonction permettant de mettre à jour l'état (state) en fonction d'une action.
  • initialState : état initial pour le premier rendu du composant.

Le reducer est une fonction pure qui reçoit en arguments l'état courant et l'action. Il doit retourner le nouvel état résultant des modifications engendrées par l'action. Comme pour useState(), l'état doit toujours être considéré en lecture seul et c'est un nouvel objet qui doit être retourné comme nouvel état. Par convention, on utilise une instruction switch / case avec un cas pour chaque type d'action.

L'utilisation d'un reducer va grandement simplifier notre composant racine. Par exemple :

import { useReducer } from 'react';
import todoReducer from './reducers/todoReducer';

function App() {
    const [state, dispatch] = useReducer(todoReducer, {
        theme: 'primary',
        todoList: [],
    });

    function addTodo(content) {
        dispatch({
            type: 'ADD_TODO',
            content,
        });
    }

    function handleThemeChange(e) {
        dispatch({
            type: 'SET_THEME',
            theme: e.target.value,
        });
    }

    return (
        <ThemeContext.Provider value={state.theme}>
            <div>
                <select value={state.theme} onChange={handleThemeChange}>
                    <option value="primary">Rouge</option>
                    <option value="secondary">Bleu</option>
                </select>
            </div>
        </ThemeContext.Provider>
    );
}
export default App;

Dans le dossier src, créez un dossier reducers et placez-y un fichier todoReducer.js :

function todoReducer(state, action) {
    switch (action.type) {
       case 'SET_THEME': {
           return {
               ...state,
               theme: action.theme,
           };
       }
       case 'ADD_TODO': {
           // ...
       }
       default: {
           throw new Error('action inconnue');
       }
    }
}
export default todoReducer;

7.4 Autres hooks

  • useRef() est un hook natif permettant de référencer une valeur modifiable qui n'est pas utilisée pour le rendu. Autrement dit, useRef() permet de créer une variable contenant une valeur disponible entre les différents rendus d'un composant et que la modification de cette valeur ne déclenche pas un nouveau rendu.
  • useMemo() est un hook natif permettant de mettre en cache une valeur pour gagner en performance (on appelle cela une valeur mémorisée).
  • useCallback() est un hook natif permettant de mettre en cache une fonction et de conserver la même référence à cette fonction entre les rendus (on appelle cela une fonction de rappel mémorisée).
  • useEffect() est un hook natif permettant aux composants de se synchroniser avec des systèmes externes en permettant d'exécuter du code après le rendu d'un composant. Pour rappel, le corps d'un composant doit être pur car React a absolument besoin que le rendu d'un composant soit pur. Les effets permettent de créer des effets de bord qui sont causés par le rendu lui-même et non par des interactions utilisateurs.

7.5 UseEffect

La syntaxe pour utiliser un hook useEffect() est la suivante :

import { useEffect } from 'react';

function MonComposant() {
    useEffect(() => {
        // Le code ici sera exécuté après chaque rendu
    });
}

Par défaut, un effet est exécuté après chaque rendu ce qui n'est souvent pas le comportement souhaité. Il est possible de préciser qu'un effet ne doit être ré-exécuté que si ses dépendances changent. Les dépendances se déclarent dans un tableau passé en deuxième argument à useEffect(fonction, dépendances). Par exemple :

useEffect(() => {
    // Exécuté qu'une seule fois après le rendu initial
}, []);

useEffect(() => {
    // Exécuté après le rendu initial et à chaque fois que a ou b change
}, [a, b]);

Le hook useLayoutEffect() est très similaire à useEffect(), la différence est qu'il s'exécute de manière synchrone après que toutes les mutations du DOM prévues par React aient eu lieu. Il permet donc d'inspecter / modifier le DOM juste avant que le navigateur ne l'affiche (phase painting). Autrement dit, useLayoutEffect() va bloquer l'affichage du rendu, afin d'éventuellement faire des modifications, puis créer un nouveau rendu de manière synchrone. Pour cette raison, il doit être utiliser de manière très parcimonieuse. Privilégiez useEffect() partout là où s'est possible. Voici un exemple :

import { useState, useRef, useLayoutEffect } from 'react';

export default function App() {
    const [width, setWidth] = useState(0);
    const buttonRef = useRef(null);

    useLayoutEffect(() => {
       const infos = buttonRef.current.getBoundingClientRect();
        setWidth(infos.width);
    }, []);

  return (
        <>
            <button ref={buttonRef}>Je suis un bouton</button>
            <p>{width}</p>
        </>
    );
}

7.6 Formulaire

Le hook useForm() est le hook principal de la librairie. Il permet de gérer un formulaire. Voici les options possibles :

useForm({
    mode: 'onSubmit',
    reValidateMode: 'onChange',
    defaultValues: {},
    resolver: undefined,
    context: undefined,
    criteriaMode: "firstError",
    shouldFocusError: true,
    shouldUnregister: false,
    shouldUseNativeValidation: false,
    delayError: undefined
})

La méthode handleSubmit() permet de prévenir le comportement de la soumission du formulaire, c'est-à-dire le rafraîchissement de la page. Elle peut prendre deux fonctions de rappel en argument :

  1. Le premier argument est une fonction de rappel qui est invoquée lorsque le formulaire est envoyé (lorsque le bouton de type submit, qui est le type par défaut des boutons dans les formulaires, est cliqué). Cette fonction de rappel reçoit en arguments les données du formulaire et l'objet d'événement.
  2. Le deuxième argument est une fonction de rappel qui est invoquée lorsqu'une erreur est émise lors de l'envoi du formulaire. Cette fonction de rappel reçoit en arguments les erreurs du formulaire et l'objet d'événement.

La méthode register() permet d’enregistrer un champ et d'y appliquer des règles de validation. Lorsqu'un champ est enregistré sa valeur sera disponible lors de l'envoi (dans l'objet data de la première fonction de rappel passé à handleSubmit()) et lors de la validation. Il faut obligatoirement fournir une clé comme nom du champ à enregistrer. Ce nom doit être unique pour le formulaire.

Voici un exemple simple, nous verrons à quoi sert register() juste après :

import { useForm } from "react-hook-form";

export default function App() {
    const { register, handleSubmit } = useForm();
    const onSubmit = (data, e) => console.log(data, e);
    const onError = (errors, e) => console.log(errors, e);

    return (
        <form onSubmit={handleSubmit(onSubmit, onError)}>
            <input {...register("firstName")} />
            <input {...register("lastName")} />
            <button type="submit">Submit</button>
        </form>
    );
}

// Équivalent

const { onChange, onBlur, name, ref } = register('prenom');

<input
    onChange={onChange}
    onBlur={onBlur}
    name={name}
    ref={ref}
/>

La méthode getValues() permet de lire les valeurs du formulaire sans re-déclencher un rendu ni s'abonner aux changements des champs avec des écouteurs d'événement. Prenons un exemple avec deux champs et supposons que nous ayons entré a dans le champ test et b dans le champ test1 :

import { useForm } from "react-hook-form";

export default function App() {
    const { register, getValues } = useForm();

    return (
        <form>
            <input {...register("test")} />
            <input {...register("test1")} />
            <button type="button" 
                onClick={() => {
                    const valeurs = getValues(); // { test: "a", test1: "b" }
                    const valeurDunChamp = getValues("test"); // "a"
                    const valeurDePlusieursChamps = getValues(["test", "test1"]); // ["a", "b"]
                }}
            >
                Get Values
            </button>
        </form>
    );
}

La méthode watch() permet de surveiller un ou plusieurs champs et de retourner leurs valeurs. C'est utile pour afficher à un autre endroit la valeur d'un champ. Contrairement à getValues(), la méthode watch() déclenche de nombreux rendus supplémentaires, à chaque changement du ou des champs surveillés.

8. Tableaux

8.1 Filter

Pour enlever un élément d'un tableau, on peut utiliser filter() car cette méthode crée un nouveau tableau sans modifier le tableau sur lequel elle est appliquée :

const personneASupprimer = {
    id: 42,
    prenom: 'John',
    nom: 'Doe',
};

setPersonnes(
    personnes.filter(p => p.id !== personneASupprimer.id)
);

8.2 Map

Pour transformer un tableau par exemple pour modifier un élément ou tous les éléments d'un tableau, il faut utiliser map() comme dans l'exemple suivant :

const nextCounters = counters.map((c, i) => {
    if (i === index) {
        return c + 1;
    } else {
        return c;
    }
});

setCounters(nextCounters);

8.3 Manipulation de tableau

Pour ajouter un élément à un index spécifique dans un tableau contenu dans l'état il faut utiliser la méthode slice et l'opérateur spread :

const [tableau, setTableau] = useState([]);

setTableau(
    [
        ...tableau.slice(0, indexInsertion),
        42,
        ...tableau.slice(indexInsertion),
    ]
);

Pour trier un tableau contenu dans l'état il faut créer une copie du tableau avec l'opérateur spread et ensuite utiliser par exemple sort() ou reverse() sur cette copie, comme dans l'exemple suivant :

const nouveauTableau = [...tableau];

tableau.reverse();
setTableau(nouveauTableau);

9. React Router

Pour en savoir plus sur React Router, consultez la documentation officielle.

9.1 Installation

React Router va déclencher le chargement de composants sur certaines URLs tout en restant sur la même page. Un router est donc essentiel à une application complexe car il permet de faciliter le développement de la navigation des utilisateurs dans votre application. React n'a pas de librairie officielle pour le router, nous allons utiliser la plus populaire qui est React Router. Installez la librairie pour le Router :

npm i react-router-dom

9.2 Initialisation

Pour utiliser le Router, il faut utiliser le RouterProvider mis à disposition par la librairie. Il est nécessaire de lui passer les routes de l'application que le Router doit rendre grâce à la propriété router. Une route est constituée a minima d'un chemin (path) et d'un composant associé. Par exemple dans index.js :

import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
    <StrictMode>
        <RouterProvider router={router} />
    </StrictMode>
);

Le router doit contenir un tableau de routes, qui peuvent contenir comme dans notre exemple des routes imbriquées, grâce à la propriété children qui peut elle-même prendre un tableau de routes. Une route a au minima une propriété path et une propriété element. Le chemin est simplement la route associée à l'élément React devant être rendu par le Router. Au même niveau que le fichier index.js, créez un fichier router.js, avec :

import { createBrowserRouter } from 'react-router-dom';
import App from './App';
import Homepage from './pages/Homepage/Homepage';
import Profile from './pages/Profile/Profile';

export const router = createBrowserRouter([
    {
        path: '/',
        element: <App />,
        children: [
            {
                path: '/',
                element: <Homepage />,
            },
            {
                path: '/profile',
                element: <Profile />,
            },
        ],
    },
]);

Voici comment créer les fichiers pages/Homepage/Homepage.js et pages/Profile/Profile.js :

export default function Homepage() {
    return <h2>Homepage</h2>;
}

// Et

export default function Profile() {
    return <h2>Profile</h2>;
}

Un composant Outlet doit être utilisé dans un élément parent rendu par le Router (ici notre composant App) pour définir où doivent être affichées ses routes enfants (déclarées dans la propriété children). Dans le composant App.js, qui est rendu par le Router quel que soit le chemin, nous ajoutons un composant Outlet :

import Footer from './components/Footer/Footer';
import Header from './components/Header/Header';
import { Outlet } from 'react-router-dom';

function App() {
    return (
        <div>
            <Header />
            <div>
                <Outlet />
            </div>
            <Footer />
        </div>
    );
}

export default App;

Le composant Link fourni par le Router, permettant à l'utilisateur de naviguer sur une autre page en cliquant dessus. Le composant prend une propriété to qui permet de définir le chemin cible pour la navigation. Dans le composant Header.js, nous utilisons des composants Link pour créer des liens :

import { Link } from 'react-router-dom';

function Header() {
    return (
        <header>
            <ul>
                <Link to="/">Homepage</Link>
                <Link to="/profile">Profile</Link>
            </ul>
        </header>
    );
}

export default Header;

9.3 Propriétés

La propriété index prend un booléen. Si la propriété vaut true alors la route est une route index. Les routes index sont affichées au niveau du composant Outlet de leur composant parent lorsque l'URL correspond à l'URL de la route parente. Reprenons notre exemple, et modifions le fichier router.js. La route index sera affichée sur le chemin / qui est le chemin du composant parent du composant Homepage. Par exemple :

export const router = createBrowserRouter([
    {
        path: '/',
        element: <App />,
        children: [
            {
                index: true,
                element: <Homepage />,
            },
            {
                path: '/profile',
                element: <Profile />,
            },
        ],
    },
]);

La propriété caseSensitive permet de définir si la correspondance de l'URL à une route doit respecter la casse (minuscule / majuscule) ou non. Par exemple :

export const router = createBrowserRouter([
    {
        path: '/',
        element: <App />,
        errorElement: <ErrorPage />,
        children: [
            {
                element: <Homepage />,
            },
            {
                path: '/profile',
                element: <Profile />,
                caseSensitive: true,
            },
        ],
    },
]);

La propriété errorElement permet de rendre un composant différent lorsqu'une erreur survient lors du rendu du composant passé à element ou si aucune route ne correspond. Modifions maintenant le fichier router.js pour utiliser la propriété que nous avons vu :

import ErrorPage from './pages/ErrorPage/ErrorPage';

export const router = createBrowserRouter([
    {
        path: '/',
        element: <App />,
        errorElement: <ErrorPage />,
        children: [
            {
                index: true,
                element: <Homepage />,
            },
            {
                path: '/profile',
                element: <Profile />,
                caseSensitive: true,
            },
        ],
    },
]);

Vous pouvez mettre la propriété errorElement au sommet de l'arbre des composants pour gérer toutes les erreurs de la branche au même endroit. Le hook useRouteError() permet d'accéder à l'erreur dans le composant passé à errorElement. Il est également possible de le mettre plus bas dans l'arbre pour gérer différemment les erreurs suivants les composants. Créez enfin le composant ErrorPage dans le dossier pages/ErrorPage :

import React from 'react';
import { useRouteError } from 'react-router-dom';

export default function ErrorPage() {
    const error = useRouteError();

    console.log(error);

    return (
        <>
            <h2>ErrorPage</h2>
            <p>{error.message || error.statusText}</p>
        </>
    );
}

9.4 NavLink

Le composant NavLink est un composant Link qui sait s'il est actif. Par défaut, la class active est ajoutée sur le composant NavLink lorsqu'il est actif (c'est-à-dire si l'URL correspond à la route to du lien). Il est possible d'utiliser la propriété end sur le composant NavLink afin que le composant ne soit considéré comme actif que si son chemin est actif et non celui de ses descendants ou routes du même niveau. Il est également possible de personnalisé le nom des classes ou les styles appliqués au composant suivant qu'il est actif ou non. Pour cela, il faut utiliser une fonction qui reçoit isActive en argument. Voici un exemple du fonctionnement :

import { NavLink } from "react-router-dom";

function Menu() {
    let activeStyle = {
        textDecoration: "underline",
    };
    let activeClassName = "underline";

    return (
        <nav>
            <ul>
                <li>
                    <NavLink
                        to="/articles"
                        style={({ isActive }) =>
                            isActive ? activeStyle : undefined
                        }
                    >Articles</NavLink>
                </li>
                <li>
                    <NavLink
                        to="/profile"
                        className={({ isActive }) =>
                            isActive ? activeClassName : undefined
                        }
                    >Profil</NavLink>
                </li>
            </ul>
        </nav>
    );
}

9.5 UseMatch

Le hook useMatch() permet d'obtenir des données sur une route avec le chemin spécifié en fonction de l'URL actuelle. Le hook retourne null si l'URL ne correspond pas au chemin de la route passée en argument. Si le chemin correspond, elle retourne un objet de cette forme :

{
    params: {}
    pathname: "/profile",
    pathnameBase: "/profile",
    pattern: {
        caseSensitive: false
        end: true
        path: "/profile"
    }
}

10. Génération UUID

L'instruction crypto.randomUUID() est une API Web qui est compatible avec tout navigateur et permet de générer un UUID v4. C'est typiquement le genre d'identifiant que vous auriez dans une base de données. Par exemple :

crypto.randomUUID()

Pour en savoir plus sur la génération UUID de React, consultez la documentation officielle.