Aller au contenu

React

Quelques définitions

  • Le page d'accueil du site react.dev ainsi que react.dev/learn proposent une bonne introduction aux composants React.
  • Composition : combiner des composants avec d'autres composants.
  • Un composant React est une fonction qui retourne du JSX (du HTML dans du JS).

Projet reactjs avec vite

npx create-vite@latest my-react-app --template react
cd my-react-app
npm install
# Lancer un serveur de développement
npm run dev
# Créer un build de production
npm run build

Dans le dossier src, créer le dossier components et y ajouter un fichier Hello.jsx.

  • Ajouter un composant Hello (dans le fichier Hello.jsx)qui prend un prop name et affiche Hello, {name}.
  • Si le linteur signale un problème de typage du compsant, installer la librairie prop-types (npm install --save-dev prop-types) et renseigner les types comme indique dans les exemples de code.
  • Ajouter un composant StateDemo (dans le fichier StateDemo.jsx) qui illustre l'état local avec useState.
Hello.jsx
import { useState } from "react";
import PropTypes from "prop-types";

export default function Hello({ name }) {
    return <h1>Hello, {name}</h1>;
}

Hello.propTypes = {
    name: PropTypes.string.isRequired,
};
StateDemo.jsx
import { useState } from "react";
export function StateDemo() {
    const [count, setCount] = useState(0);
    return (
        <>
            <p>Vous avez cliqué {count} fois</p>
            <button onClick={() => setCount(count + 1)}>Cliquez ici</button>
        </>
    );
}

Exercices

Basique

  1. Créer un composant Counter qui affiche un bouton + et un bouton - pour incrémenter et décrémenter un compteur. Initialiser le compteur à 42.
  2. Créer un composant ToLowerCase qui prend un prop (ou un argument) text et affiche le texte en minuscules. (💡 astuce: string.toLowerCase())
  3. Créer un composant EurToYen qui permet de saisir des euros et affiche le montant en yens (1 euro = 130 yens). (💡 astuce: utiliser <input type="number" />)
  4. Créer un composant ShowMax qui prend deux nombres en props et affiche le plus grand.
  5. Créer un composant Guess qui affiche un zone éditable numérique et un bouton. A chaque fois que l'utilisateur clique sur le bouton, le composant génère un nouveau nombre aléatoire entre 1 et 10 et affiche si le nombre saisi est trop grand, trop petit ou si c'est le bon nombre. (💡 astuce: utiliser Math.random() pour générer un nombre aléatoire).
  6. Créer un composant CountConsonantsAndVowels qui prend un prop text et affiche le nombre de consonnes et de voyelles (aeiuyo) dans le texte. Instancier ce composant dans <App> et faire en sorte que le prop soit alimenté via la valeur d'un texte editable isntancié dans <App>. (💡 astuce: utiliser string.match(/[aeiuyo]/gi) pour compter les voyelles).
  7. Créer un composant Palindrome qui prend un prop text et affiche si le texte est un palindrome ou non. (💡 astuce: utiliser string.split('').reverse().join('') pour inverser une chaîne de caractères).
  8. Créer un composant Fibonacci qui prend un prop n et affiche le n-ième terme de la suite de Fibonacci.
Solutions
ExoCounter.jsx
import { useState } from "react";

export default function ExoCount(){
  const initialValue = 42;
  // const [état, fonction de maj de l'état] = useState(val par défaut);
  const [count, setCount] = useState(initialValue);

  return <>
  <button onClick={() => setCount(count + 1)}>+</button>
  {count}
  <button onClick={() => setCount(count - 1)}>-</button>
  <button onClick={() => setCount(initialValue)}>Reset</button>
  </>;
}
ExoCounter.jsx
import PropTypes from "prop-types";

export default function ExoLowerCase({ text }) {
  const lowerCased = text.toLowerCase();
  return (
    <>
      <ul>
        <li>
          Méthode 1 en calculant dans la partie code du composant: {lowerCased}
        </li>
        <li>Méthode 2 directement dans le html: {text.toLowerCase()}</li>
      </ul>
    </>
  );
}

// la prp text est une chaîne de caractères obligatoire
ExoLowerCase.propTypes = {
  text: PropTypes.string.isRequired,
};
EuroToYen.jsx
import { useState } from "react";

export default function EuroToYen() {
  const [euro, setEuro] = useState(0);
  const yen = euro * 165;
  return (
    <>
      <label htmlFor="euro">Euros</label>
      <input
        type="number"
        name="euro"
        value={euro}
        onChange={(event) => setEuro(event.target.value)}
      />
      Yen: {yen}
    </>
  );
}
ShowMax.jsx
import PropTypes from "prop-types";

export default function ShowMax({ a, b }) {
  const max = a >= b ? a : b;
  let maxIf = a;
  if (a > b) {
    maxIf = a;
  } else {
    maxIf = b;
  }
  return (
    <>
      Max of {a} and {b} is {max}, {a >= b ? a : b} , {maxIf}
    </>
  );
}

ShowMax.propTypes = {
  a: PropTypes.number.isRequired,
  b: PropTypes.number.isRequired,
};
CountConsonantsAndVowels.jsx
import PropTypes from "prop-types";

export default function CountConsonantsAndVowels({ word }) {
  // word.match(/[aeiuyo]/gi)? : renvoie un tableau qui peut être null et ? renvoie null au lieu de crasher
  // ?? 0 Si le résultat est null, on renvoie 0
  const vowelCount = word.match(/[aeiuyo]/gi)?.length ?? 0;
  return (
    <>
      Vowels of {word}: {vowelCount}. Consonants: {word.length - vowelCount}
    </>
  );
}

CountConsonantsAndVowels.propTypes = {
  word: PropTypes.string.isRequired,
};

Listes

List de todos
import { useState } from "react";

const originalTodoItems = [
  {
    id: 1,
    title: "Cours de react",
    done: false,
  },
  {
    id: 2,
    title: "Cours d'ElectronJS",
    done: false,
  },
  {
    id: 3,
    title: "Filter, map",
    done: true,
  },
];

export default function TodoList() {
  const [todoItems, setTodoItems] = useState(originalTodoItems);

  function handleCheck(checkedTodoItem) {
    // Création d'une nouvelle liste qui change le done de l'émément coché
    const newTodoItems = todoItems.map((todoItem) => {
      // On change l'état done de l'élément coché
      if (todoItem.id === checkedTodoItem.id) {
        todoItem.done = !todoItem.done;
      }
      return todoItem;
    });
    setTodoItems(newTodoItems);
  }

  function setAllTodosDone(done) {
    setTodoItems(
      todoItems.map((todoItem) => {
        todoItem.done = done;
        return todoItem;
      })
    );
  }

  // l'attribut key permet d'optimiser le rendu côté react
  const todoElements = todoItems.map((todoItem) => (
    <li key={todoItem.id}>
      id: {todoItem.id} - <b>{todoItem.title}</b> -{" "}
      <i>{todoItem.done ? "Done" : "Not done"}</i>
      <input
        type="checkbox"
        name="done"
        checked={todoItem.done}
        onChange={() => handleCheck(todoItem)}
      />
    </li>
  ));
  return (
    <>
      <h2>todo list</h2>
      <button onClick={() => setAllTodosDone(true)}>All done</button>
      <button onClick={() => setAllTodosDone(false)}>All undone</button>
      <ul>{todoElements}</ul>
    </>
  );
}
List de todos enrgistrée
import { useEffect, useState } from "react";

const originalTodoItems = [
  {
    id: 1,
    title: "Cours de react",
    done: false,
  },
  {
    id: 2,
    title: "Cours d'ElectronJS",
    done: false,
  },
  {
    id: 3,
    title: "Filter, map",
    done: true,
  },
];

export default function TodoListStored() {
  const [todoItems, setTodoItems] = useState(originalTodoItems);

  // Hook (commence par use) qui synchronise l'état avec une donnée externe
  // On doit lui spécifier en callback la donnée à charger et
  // en deuxième argument les états qui en dépendent et qui ne se font pas set
  useEffect(() => {
    const storageTodoItemsString = localStorage.getItem("todoItems");
    if (storageTodoItemsString != null) {
      const storageTodoItems = JSON.parse(storageTodoItemsString);
      setTodoItems(storageTodoItems);
    }
  }, []);

  function saveAndSetTodoItems(todoItems) {
    localStorage.setItem("todoItems", JSON.stringify(todoItems));
    setTodoItems(todoItems);
  }

  function handleCheck(checkedTodoItem) {
    const newTodoItems = todoItems.map((todoItem) => {
      if (todoItem.id === checkedTodoItem.id) {
        todoItem.done = !todoItem.done;
      }
      return todoItem;
    });
    saveAndSetTodoItems(newTodoItems);
  }

  function setAllTodosDone(done) {
    saveAndSetTodoItems(
      todoItems.map((todoItem) => {
        todoItem.done = done;
        return todoItem;
      })
    );
  }

  const todoElements = todoItems.map((todoItem) => (
    <li key={todoItem.id}>
      id: {todoItem.id} - <b>{todoItem.title}</b> -{" "}
      <i>{todoItem.done ? "Done" : "Not done"}</i>
      <input
        type="checkbox"
        name="done"
        checked={todoItem.done}
        onChange={() => handleCheck(todoItem)}
      />
    </li>
  ));
  return (
    <>
      <h2>Todo List local storage</h2>
      <button onClick={() => setAllTodosDone(true)}>All done</button>
      <button onClick={() => setAllTodosDone(false)}>All undone</button>
      <ul>{todoElements}</ul>{" "}
    </>
  );
}
CRUD d'une liste to todos
import { useEffect, useState } from "react";

export default function TodoListCrud() {
  const [todoItems, setTodoItems] = useState([]);
  const [newTodoItemTitle, setNewTodoItemTitle] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  useEffect(() => {
    const storageTodoItemsString = localStorage.getItem("todoItems");
    if (storageTodoItemsString != null) {
      const storageTodoItems = JSON.parse(storageTodoItemsString);
      setTodoItems(storageTodoItems);
    }
  }, []);

  function addTodoItem() {
    if (newTodoItemTitle.length == 0) {
      setErrorMessage("Incorrect length");
      return;
    }
    setErrorMessage("");
    const todoItem = {
      id: todoItems.length + 1,
      title: newTodoItemTitle,
      done: false,
    };
    // ... spread operator
    // ...todoItems => todoItems[0], todoItems[1], ...
    // Création d'une nouvelle liste qui reprend todoItems + todoItem
    const newTodoItems = [...todoItems, todoItem];
    saveAndSetTodoItems(newTodoItems);
  }

  function saveAndSetTodoItems(todoItems) {
    localStorage.setItem("todoItems", JSON.stringify(todoItems));
    setTodoItems(todoItems);
  }

  function handleCheck(checkedTodoItem) {
    const newTodoItems = todoItems.map((todoItem) => {
      if (todoItem.id === checkedTodoItem.id) {
        todoItem.done = !todoItem.done;
      }
      return todoItem;
    });
    saveAndSetTodoItems(newTodoItems);
  }

  function setAllTodosDone(done) {
    saveAndSetTodoItems(
      todoItems.map((todoItem) => {
        todoItem.done = done;
        return todoItem;
      })
    );
  }

  const todoElements = todoItems.map((todoItem) => (
    <li key={todoItem.id}>
      id: {todoItem.id} - <b>{todoItem.title}</b> -{" "}
      <i>{todoItem.done ? "Done" : "Not done"}</i>
      <input
        type="checkbox"
        name="done"
        checked={todoItem.done}
        onChange={() => handleCheck(todoItem)}
      />
    </li>
  ));

  const style = {
    border: "1px solid green",
    "border-radius": "5px",
    "background-color": "#FAF",
  };
  return (
    <div style={style}>
      <h2>Todo List CRUD + localStorage</h2>
      <button onClick={() => setAllTodosDone(true)}>All done</button>
      <button onClick={() => setAllTodosDone(false)}>All undone</button>
      <div>
        <label htmlFor="title">Title:</label>
        <input
          type="text"
          name="title"
          minLength={1}
          placeholder="My new task"
          value={newTodoItemTitle}
          onChange={(event) => setNewTodoItemTitle(event.target.value)}
        />
        <button onClick={addTodoItem}>Add</button>
        <button onClick={() => addTodoItem()}>Add</button>
        <br />
        {errorMessage}
      </div>
      <ul>{todoElements}</ul>{" "}
    </div>
  );
}
  1. Créer un composant ShowLengths qui prend un prop items (un tableau de chaînes de caractères) et affiche chaque élément suivi de sa longueur. (💡 astuce: utiliser string.length pour obtenir la longueur d'une chaîne de caractères).

    • Par exemple, si items = ['un', 'deux', 'trois'], le composant affiche:

      un (2)
      deux (4)
      trois (5)
      
  2. Créer un composant ShowAlternating qui prend un prop items (un tableau de chaînes de caractères) et affiche les éléments de la liste en alternant les couleurs de fond (par exemple, une ligne sur deux en gris). (💡 astuce: utiliser index % 2 === 0 pour tester si l'index est pair).

    • Par exemple: si items = ['un', 'deux', 'trois'], le composant affiche:

      un (avec fond gris)
      deux (sans fond)
      trois (avec fond gris)
      
  3. Créer un composant LoggerComponent qui affiche un champ de texte éditable et un bouton Ajouter. Chaque fois que l'utilisateur clique sur le bouton Ajouter, le texte saisi est ajouté à une liste. Afficher la liste des textes saisis.

    • Par exemple, si l'utilisateur saisit "un", "deux" et "trois", le composant affiche:

      un
      deux
      trois
      
  4. Créer un composant LoggerComponentExtended qui affiche un champ de texte éditable et un bouton Ajouter. Chaque fois que l'utilisateur clique sur le bouton Ajouter, le texte saisi est ajouté à une liste contenant également l'horodatage de l'ajout. Afficher la liste.

    • Par exemple, si l'utilisateur saisit "un", "deux" et "trois", le composant affiche:

      2022-01-01 12:00:00 un
      2022-01-01 12:00:01 deux
      2022-01-01 12:00:02 trois
      
  5. Créer un composant EditableLogger qui affiche un champ de texte éditable et un bouton Ajouter. Chaque fois que l'utilisateur clique sur le bouton Ajouter, le texte saisi est ajouté à une liste contenant également l'horodatage de l'ajout.

    • Afficher la liste en permettant d'éditer le corps (pas la date) de chaque ligne élément et de supprimer un élément.
    • Par exemple, si l'utilisateur saisit "un", "deux" et "trois", le composant affiche:

      2022-01-01 12:00:00 un [Input pour modifier le texte] [Bouton pour supprimer]
      2022-01-01 12:00:01 deux [Input pour modifier le texte] [Bouton pour supprimer]
      2022-01-01 12:00:02 trois [Input pour modifier le texte] [Bouton pour supprimer]
      
    • Ajouter les boutons sauvegarder et charger pour sauvegarder la liste dans le local storage et la charger au démarrage de l'application ou au clique sur le bouton charger.

Solutions
StringsLengths.jsx
import PropTypes from "prop-types";

/**
 * To instanciate <StringLengths items={["un", "deux", "trois"]} />
 * @param {{texts: string[]}}
 * @returns
 */
export default function StringLengths({ texts }) {
  const textsElements = texts.map((text, index) => (
    <li key={index}>text ({text.length})</li>
  ));
  return <ul>{textsElements}</ul>;
}

StringLengths.propTypes = {
  texts: PropTypes.string.isRequired,
};
ShowAlternating.jsx
import PropTypes from "prop-types";

/**
 * To instanciate <ShowAlternating items={["un", "deux", "trois"]} />
 * @param {{texts: string[]}}
 * @returns
 */
export default function ShowAlternating({ texts }) {
  const textsElements = texts.map((text, index) => (
    <li
      key={index}
      style={{ backgroundColor: index % 2 === 0 ? "grey" : "blue" }}
    >
      text ({text.length})
    </li>
  ));
  return <ul>{textsElements}</ul>;
}

ShowAlternating.propTypes = {
  texts: PropTypes.string.isRequired,
};

Composition

  1. Créer un composant SocialPost qui prend en props les propriétés author, date, content et avatar. Afficher ces propriétés de façon jolie.

    • Par exemple, si author = 'Alice', date = '2022-01-01', content = 'Hello world!' et avatar = 'alice.jpg', le composant affiche (de façon pas jolie):

      <img src="alice.jpg" alt="Avatar de Alice" />
      <h2>Alice</h2>
      <p>2022-01-01</p>
      <p>Hello world!</p>
      
  2. Créer un composant SocialPostList qui prend en prop posts (un tableau d'objets avec les propriétés author, date, content et avatar) et affiche une liste de SocialPost.

  3. Créer un composant HomePage qui affiche un logo, un titre et une liste de SocialPost. Créer un composant AboutPage qui affiche un logo, un titre et un texte de présentation. Dans le composant App, afficher les deux liens. Avec un booléen isHomePage, afficher soit la page d'accueil, soit la page "à propos" selon le lien sur lequel on clique.

Router

Le router permet de gérer les différents écrans d'une application React. Il permet de naviguer entre les différentes écrans sans recharger la page entière. En effet, une application React ne contient en réalité qu'une seule page HTML et est ré-initialisée à chaque fois qu'on charge ou recharge cette page. Le routeur permet donc de changer le composant qui affiche le contenu principal de l'écran actuel sans recharger l'application. Cette zone est appelée un outlet.

Éléments principaux

Le routeur est mis en place avec ces trois éléments principaux:

  • Router : permet de définir les routes. i.e. pour chaque écran, renseigner son composant principal et son chemin d'url.
  • Link: permet de créer un lien vers une route à l'instar de la balise <a>. Il ne faut pas utiliser <a> car cela rechargerait la page entière.
  • Outlet: permet de définir la zone où le contenu de la route actuelle sera affiché.

Router illustration

Guide

  1. Créer un projet React avec vite (npm create vite@latest react-app-with-router).
  2. Préparation
    1. Créer un composant Home qui affiche "Bienvenue sur la page d'accueil".
    2. Créer un composant About qui affiche "À propos de nous".
    3. Créer un composant Contact qui affiche "Contactez-nous".
  3. Nettoyer le composant App.

    export default function App() {
        return <></>;
    }
    
  4. Installer react-router-dom avec npm install react-router-dom.

  5. Dans src/main.jsx Créer une table de routage en ajoutant le contenu suivant:

    import { RouterProvider, createBrowserRouter } from "react-router-dom";
    import Home from "./components/Home.jsx";
    import Contact from "./components/Contact.jsx";
    import About from "./components/About.jsx";
    // Table de routage
    const router = createBrowserRouter([
    {
        path: "/",
        element: <App />,
        children: [
        {
            path: "/home",
            element: <Home />,
        },
        {
            path: "/about",
            element: <About />,
        },
        {
            path: "/contact",
            element: <Contact />,
        },
        ],
    },
    ]);
    
    • modifier la partie createRoot pour utiliser <React.StrictMode> et <RouterProvider> (c'est lui va gérer les routes de l'application).
    ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <RouterProvider router={router} />
    </React.StrictMode>
    );
    
    • Il faut aussi importer import ReactDOM from "react-dom/client";.
    • Fichier complet:
    src/main.jsx
    import React from "react";
    import ReactDOM from "react-dom/client";
    
    import "./index.css";
    import App from "./App.jsx";
    
    import { RouterProvider, createBrowserRouter } from "react-router-dom";
    import Home from "./components/Home.jsx";
    import Contact from "./components/Contact.jsx";
    import About from "./components/About.jsx";
    
    const router = createBrowserRouter([
    {
        path: "/",
        element: <App />,
        children: [
        {
            path: "/home",
            element: <Home />,
        },
        {
            path: "/about",
            element: <About />,
        },
        {
            path: "/contact",
            element: <Contact />,
        },
        ],
    },
    ]);
    
    ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <RouterProvider router={router} />
    </React.StrictMode>
    );
    
  6. Dans le composant App, ajouter un lien vers la page d'accueil, la page "à propos" et la page de contact ({" - "} permet de forcer un espace avant et après le -).

    import { Link } from "react-router-dom";
    export default function App() {
        return (
        <>
            <h1>React router demo</h1>
            <nav>
                <Link to="/home">Accueil</Link>{" - "}
                <Link to="/about">À propos</Link>{" - "}
                <Link to="/contact">Contact</Link>
            </nav>
        </>
        );
    }
    
  7. Tester l'application en lançant npm run dev. Que constatez vous au niveau du rendu de votre app et de la barre d'adresse ?

  8. Ajouter le outlet dans le composant App pour afficher les pages enfants.

    import { Outlet } from "react-router-dom";
    export default function App() {
        return (
        <>
            <h1>React router demo</h1>
            <nav>
                <Link to="/home">Accueil</Link>{" - "}
                <Link to="/about">À propos</Link>{" - "}
                <Link to="/contact">Contact</Link>
            </nav>
            <Outlet />
        </>
        );
    }
    
  9. Comme les composants sont des pages, il est recommandé de les déplacer dans le dossier src/pages. Appliquer cette convention à l'avenir.

  10. On peut spécifier des paramètres dans des routes (par exemple pour afficher un item à partir d'une liste).

Astuces

  • Pour activer la colorations des parenthèses, accolades et crochets dans VSCode, activer l'option "Bracket Pair colorization" dans les paramètres.
  • L'extension indent-rainbow permet de colorer les indentations.