Illustration de l'article

Explorez React : Introduction des fondamentaux

Écrit par Nicolas Viviani

Plan

Les concepts les plus importants de React

  1. 🏁 *Introduction** - prĂ©sentation globale de la librairie React
  2. ⌚ *Installation et CLI** - comment installer et utiliser la CLI
  3. đŸ§‘â€đŸ’» *Le langage JSX** - introduction au langage JSX
  4. ✂ *Composants** - l’architecture dĂ©coupĂ©e en composants
  5. đŸ›ïž *Hooks** - le state, les effets
  6. đŸ›Łïž *Route et navigation** - navigation entre les pages
  7. đŸ—Łïž *Props** - communication entre les composants
  8. đŸ©ș *Test** - faire des tests sur son appli React
  9. ✏ *Formulaires** - gĂ©rer les interactions utilisateurs
  10. 📜 *Style** - mise en forme avec du CSS

Pour en savoir plus : react.dev


1. Introduction

Qu’est ce que React ?

React est une librairie Frontend permettant de crĂ©er des Ă©lĂ©ments d’interface rĂ©utilisable.

Popularité

React est dĂ©veloppĂ© et maintenu par Meta (Facebook) et est l’une des librairies Front les plus utilisĂ©es. De nombreux modules existent pour Ă©tendre son fonctionnement.

Quelle différence avec un framework ?

Un framework est plus lourd et plus complet qu’une librairie. React ne possĂšde que des fonctionnalitĂ©s pour crĂ©er des interfaces graphiques. Il est cependant possible de l’étendre avec des modules pour faire du routing, des tests etc.

Is React a Library or a Framework? Here’s Why it Matter


2. Installation et CLI

Pour commencer Ă  utiliser React il suffit d’une ligne de commande

npm install react react-dom

Cependant, au lieu de se lancer de zĂ©ro, il est plus aisĂ© et beaucoup plus simple d’utiliser un outil qui permet de crĂ©er un squelette de projet. Il est en existe plusieurs :

npm create vite@latest
  • CreateReactApp
npm create-react-app my-app
  • Next.js
  • Gatsby
  • 


Si on génÚre le projet avec Vite, trois fichiers importants sont générés index.html, main.jsxet App.jsx.

index.html contient un squelette de site et l’Ă©lĂ©ment racine sur lequel React va attacher notre application.

<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>

C’est main.jsx qui s’occupe de ça. Par dĂ©faut le fichier App.jsx sera rendu dans l’Ă©lĂ©ment HTML root.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Enfin, App.jsxconstitue la porte d’entrĂ©e de notre application, c’est ce qui est affichĂ©, c’est donc lĂ  qu’on commence Ă  coder en React.

React fournit Ă©galement une extension pour navigateur, les React Developer Tools qui permettent d’Ă©tendre l’inspecteur d’Ă©lĂ©ment du navigateur en y ajoutant un nouvel onglet incluant divers Ă©lĂ©ments spĂ©cifiques Ă  React


3. Le langage JSX

https://grafikart.fr/tutoriels/syntaxe-jsx-react-1314


C’est quoi le JSX ?

JSX est un langage utilisé par React permettant de mélanger Javascript et HTML.

function App() {
  const name = "World";
  return <h1>Hello {name}! </h1>;
}

export default App

En rĂ©alitĂ© il s’agit simplement d’une extension syntaxique de JavaScript, on Ă©crit pas vraiment de l’HTML mais une redĂ©finition JSX qui ressemble beaucoup.

Cette syntaxe n’est pas une syntaxe JS valide, mais JSX permet de faire ça afin de retourner du HTML plus simplement.


JSX a plusieurs avantages

  • Pas de sĂ©paration du template et de la logique
  • Reprend la syntaxe de JavaScript

Writing Markup with JSX

JavaScript in JSX with Curly Braces


Le JSX est automatiquement transpilé par React vers du Javascript classique.

Les balises HTML

Il est possible d’inclure des balises HTML dans le code JSX

function Component() {
  return <input id="name"/>
}
export default Component

Attention cependant, certaines balises diffĂšrent du HTML classique. C’est le cas notamment de la balise class qui devient className en JSX.

On notera aussi que les attributs HTML avec des tirets (Ă  l’exception des attributs aria- et data-) doivent ĂȘtre Ă©crit en JSX en utilisant du camelCase.

function Component() {
  return <input id="name" className="name" type="text" minLength="4" aria-autocomplete />
}
export default Component

Le langage JSX ne permet de renvoyer qu’un seul Ă©lĂ©ment racine. Le code ci-dessous est faux et affichera une erreur ↓

function Item() {
  return (
    <h2>Titre</h2>
    <p>Description</p>
  )
}
export default Item

Il faut donc soit tout imbriquer dans une balise <div></div> soit utiliser un <React.Fragment> ou sa syntaxe alternative <></>.

import React from 'react'
function Item() {
  return (
      <React.Fragment>
        <h2>Titre</h2>
        <p>Description</p>
      </React.Fragment>
    )
}
export default Item

Interpolation

L’interpolation est l’affichage de texte Ă  la volĂ©e. En JSX, l’interpolation s’utilise avec des accolades {}.

function App() {
  const name = "panier";
  return <div className={name}>Pour vider le {name}, cliquez ici</div>;
}

export default App

Il est possible d’interpoler plus que des variables, on peut Ă©crire du code JSX Ă  l’intĂ©rieur des accolades {}.

function ItemDescription() {
  const doesDescriptionExist = false;
  const description = "Ceci est une description";
  return (
    <div> 
      <h1>Titre</h1>
      {doesDescriptionExist ? <p>{description}</p> : <p>Pas de description</p>}
    </div>
  )
}
export default ItemDescription
function ItemDescription() {
  const doesDescriptionExist = false;
  const description = "Ceci est une description";
  return (
    <div> 
      <h1>Titre</h1>
      {doesDescriptionExist && <p>{description}</p>}
    </div>
  )
}
export default ItemDescription

En rĂ©alitĂ© on peut Ă©crire n’importe quelle expression JavaScript Ă  l’intĂ©rieur des accolades {} mais pas d’instructions. De la mĂȘme maniĂšre qu’en Javascript on ne ferait pas ↓

let i = if (code === 42) { "Loire" } else { "RhĂŽne" }

Le code ci-dessous est donc invalide ↓

function App() {
  const citation = "I'm doing a (free) operating system (just a hobby, won't be big and professional like gnu)"
  return <blockquote cite="<https://groups.google.com/g/comp.os.minix/c/dlNtH7RRrGA/m/SwRavCzVE7gJ>">
      <p>{citation}</p>
      <footer>{
        if (citation.includes("free")) {
          "Linus Torvalds"
        } else {
          "Bill Gates"
        }
      }</footer>
    </blockquote>
}
export default App

ÉvĂ©nements

On peut interpoler des fonctions et mĂȘme les Ă©crire directement dans les accolades {} grĂące aux lambdas notamment pour gĂ©rer les Ă©vĂ©nements.

function Button() {
  const handleHover = (e) => {
    alert("Vous avez passé la souris")
  }
  return <button onMouseOver={handleHover}>Passez la souris ici</button>
}
function Button() {
  return <button onClick={(e) => alert("Vous avez cliqué")}>Cliquez ici</button>
}
function handleKeyPress(e) {
  alert("Vous avez appuyé sur une touche")
}

function Button() {
  return <button onKeyDown={handleKeyPress}>Appuyez sur une touche</button>
}

En utilisant l’interpolation, on peut aussi crĂ©er des listes grĂące Ă  la fonction map.

function List() {
  const todos = [
    "Se former sur React",
    "Faire un site en React",
    "Ajouter React sur mon CV"
  ]
  return <ul>
    {todos.map((todo) => (<li key={todo}>{todo}</li>))}
  </ul>
}
export default List

On remarque l’attribut key dans la balise li, en React cela est obligatoire dans une liste et permet d’identifier les diffĂ©rents Ă©lĂ©ments de maniĂšre unique.

Keeping list items in order with key

Démonstration vidéo du problÚme

Une clĂ© unique key est important pour React afin qu’il manipule les bonnes donnĂ©es lors des rendus.


4. Composants


Créer des composants

En React, un composant est un Ă©lĂ©ment d’interface rĂ©utilisable et indĂ©pendant.

C’est en rĂ©alitĂ© une simple fonction (dans le cadre des composants fonctionnels) qui retourne un ou plusieurs Ă©lĂ©ments de balisage. Ici <Home> est un composant React.

function Home() {
  return <>
    <h1>Accueil</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    <button>Plus d'infos</button>
  </>
}
export default Home

Les mots rĂ©servĂ©s export default permettent de rendre le composant importable dans d’autres composants, le rendant ainsi rĂ©utilisable.


Comme dit prĂ©cĂ©demment, un composant est rĂ©utilisable, c’est Ă  dire qu’on peut l’utiliser dans un autre composant. Il existe alors une hiĂ©rarchie entre les composants.

Dans l’exemple ci-dessous, le composant <App> va retourner les composants que l’on souhaite afficher comme <Header>, <Content> et <Footer>.

import Header from "./Header";
import Content from "./Content";
import Footer from "./Footer";

function App() {
  return (
    <>
      <Header></Header>
      <Content></Content>
      <Footer></Footer>
    </>
  )
}
export default App
function Header() {
  return (
      <nav>
        <ul>
          <li>Accueil</li>
          <li>À propos</li>
          <li>Contact</li>
        </ul>
      </nav>
    )
}
export default Header

<App> est donc un composant parent tandis que <Header> est l’un des composants enfants de <App>.


DĂ©coupe en composants

Construire une application avec React peut vite amener à de mauvaises pratiques étant donné la liberté qui nous est laissée, ainsi il faut retenir une chose, en React il faut penser React.

Une interface est un ensemble de divers composants qui vont chacun servir des buts diffĂ©rents avec leur propre logique, qui vont ĂȘtre rĂ©utilisĂ©s, qui sont hiĂ©rarchisĂ©s et qui vont interagir entre eux.

Toute la difficultĂ© consiste donc Ă  savoir comment dĂ©couper notre interface efficacement. Cela dĂ©pend de chaque cas et chaque choix doit ĂȘtre minutieusement rĂ©flĂ©chi.


Organisation du code

React n’impose aucune architecture ni organisation de code, c’est donc Ă  nous de gĂ©rer de quelle maniĂšre regrouper et hiĂ©rarchiser nos fichiers et dossiers.

Il existe cependant des frameworks React qui sont considérés comme plus opinionated comme Next.js et Remix, qui poussent une certaine architecture.

Une maniĂšre de faire pourrait ĂȘtre de sĂ©parer nos fichiers par utilitĂ©. Ainsi on placerait tous les composants d’Ă©lĂ©ments rĂ©utilisable comme un composant <Bouton> ou <Sidebar> dans un dossier components. Les composants correspondant Ă  des pages de notre site et qui rĂ©utilisent plusieurs composants Ă©lĂ©mentaires pourraient ĂȘtre placĂ©s dans un dossier pages. Les hooks personnalisĂ©s que l’on crĂ©erait se placeraient dans un dossier hooks etc.

└── /src
    ├── /components
    ├── /pages
    ├── /hooks
    ├── /services
    └── /assets

Chaque composant serait regroupé dans son propre dossier en y incluant tous les fichiers le concernant (style, test etc.).

└── /src
    └── /components
        ├── Button
            ├── Button.jsx
            ├── Button.style.css
            └── Button.test.jsx
        └── Sidebar
            ├── Sidebar.jsx
            ├── Sidebar.style.css
            └── Sidebar.test.jsx

Mais on aurait trĂšs bien pu avoir un dossier regroupant tous les styles et un autre regroupant tous les tests.

En bref, il existe plusieurs maniĂšres d’organiser son code, cela dĂ©pend de ses besoins ou mĂȘme de ses prĂ©fĂ©rence


5. Hooks

https://grafikart.fr/tutoriels/react-hook-useState-1327

https://grafikart.fr/tutoriels/react-hook-useeffect-1328

https://grafikart.fr/tutoriels/react-hook-usememo-1330

https://grafikart.fr/tutoriels/react-hook-useref-1331

https://grafikart.fr/tutoriels/react-contextes-1335

https://grafikart.fr/tutoriels/react-hook-personnalise-1329


C’est quoi un hook ?

Les hooks sont des fonctions React qui permettent de bĂ©nĂ©ficier d’un Ă©tat local et d’autres fonctionnalitĂ©s sans avoir Ă  Ă©crire de composant de classe. Ils sont un moyen d’utiliser les composants fonctionnels avec les mĂȘmes possibilitĂ©s qu’offre une classe.

Ce sont de simples fonctions qui commencent toujours par « use ». Par exemple useState est un hook.

Voici une liste non exhaustive de différents hooks :

  • [useState](<https://react.dev/reference/react/useState>) : ajoute un Ă©tat Ă  un composant, une sorte de mĂ©moire entre les rendus. Plus d’infos
  • [useEffect](<https://react.dev/reference/react/useEffect>) : gĂ©nĂšre des effets de bord (side effect) lors de modification de donnĂ©es.
  • [useContext](<https://react.dev/reference/react/useContext>) : fait passer des donnĂ©es vers des composants enfants sans utiliser de props.
  • [useRef](<https://react.dev/reference/react/useRef>) : similaire Ă  useState mais pour des informations qui ne doivent pas ĂȘtre rendues.
  • [useMemo](<https://react.dev/reference/react/useMemo>) : met en cache le rĂ©sultat d’un calcul long et coĂ»teux
  • tous les autres hooks sont listĂ©s ici


Quelques remarques sur les hooks

  • Les hooks ne peuvent s’utiliser que dans des composants React et uniquement des composants fonctionnels.
  • Les hooks doivent systĂ©matiquement ĂȘtre appelĂ© Ă  la racine d’un composant. Ils ne peuvent donc pas ĂȘtre appelĂ©s Ă  l’intĂ©rieur d’une boucle, d’une condition ou d’une fonction imbriquĂ©e.
export default function App () {
  if (
) {
    const [x, setX] = useState(0)
  }
  return x

Dans le code ci-contre, on dĂ©clare un hook dans un if ce qui n’est pas autorisĂ© par React.

  • Ils commencent toujours par use
  • Ils permettent de rĂ©utiliser de la logique Ă  Ă©tat entre diffĂ©rents composants

Le hook useState

C’est le hook le plus connu et le plus utile. Il permet de garder en mĂ©moire des donnĂ©es qui seront automatiquement mises Ă  jour sur l’Ă©cran Ă  chaque modification de celles-ci.

Il s’utilise d’une façon un peu particuliĂšre :

import { useState } from 'react';

function Counter() {

  const [count, setCount] = useState(0)
  const increment = () => {
    setCount(count + 1)
  }

  return (<>
    <p>Compteur : {count}</p>
    <button onClick={increment}>Incrémenter</button>
  </>)
}
export default Counter

DĂ©clare une variable count, une fonction setCount (qui agira sur la variable count) et une valeur initiale 0.

La fonction increment utilise le setter de l’Ă©tat setCount qui mettra donc Ă  jour la variable associĂ©e count

La variable d’Ă©tat count est affichĂ©e et rĂ©actualisĂ©e Ă  chaque changement. La fonction increment est appelĂ©e Ă  chaque clic sur le bouton.


Pourquoi on n’utilise pas une simple variable ?

Parce que ça ne fonctionne pas !

When a regular variable isn’t enough

Deux raisons Ă  cela :

  • Les variables locales ne sont pas persistĂ©es entre les rendus. C’est Ă  dire que quand React va recharger la page, la variable sera remise Ă  zĂ©ro.
  • Les changements de valeurs des variables locales ne dĂ©clenchent pas de re-rendu. React ne sait pas qu’il faut recharger la page.

Il est possible d’avoir plusieurs Ă©tats dans un mĂȘme composant, il suffit d’utiliser useState plusieurs fois.

À noter que l’Ă©tat (et par consĂ©quent useState) ne se limite pas Ă  des valeurs numĂ©riques ou textuelles, on peut tout Ă  fait dĂ©clarer des objets JavaScript.

const [person, setPerson] = useState({firstname: "Youri", lastname: "Gagarine", age: 34})

En revanche, on ne peut pas utiliser de mutation pour modifier un objet Ă©tat. Le code ci-dessous ne fonctionnera donc pas :

person.firstname = "Valentina"
person.lastname = "Terechkova"
setPerson(person)

React pense qu’il s’agit du mĂȘme objet qui n’a pas Ă©tĂ© modifiĂ©, il ne le rendra donc pas Ă  nouveau pour afficher les changements.


Pour modifier une partie de l’objet il faudra donc utiliser la dĂ©composition grĂące au spread operator (...), ceci afin de transfĂ©rer le reste des donnĂ©es non modifiĂ©es dans le nouvel objet.

import { useState } from 'react';
function Person() {

  const [person, setPerson] = useState({firstname: "Youri", lastname: "Gagarine", job: "Cosmonaute"})
  const changeName = () => {
    setPerson({ ...person, firstname: "Valentina", lastname: "Terechkova"})
  }

  return (<>
    <p>Personne : {person.firstname} {person.lastname} ({person.job})</p  >
    <button onClick={changeName}>Changer le nom</button>
  </>)
}

export default Person

Le hook useEffect

Ce hook permet de lancer des effets de bord en parallĂšle de l’exĂ©cution d’une fonction.

useEffect(() => {
  console.log("effect")
}, [])

Il prend deux paramĂštres, un callback qui est la fonction qui va s’exĂ©cuter Ă  chaque fois que useEffect sera appelĂ© et un tableau de dĂ©pendance qui permet de dĂ©terminer quand il faudra appelĂ© le hook.

Attention : les effets sont une Ă©chappatoire au paradigme React. Ils doivent ĂȘtre utilisĂ©s uniquement pour sortir de l’environnement React, pour synchroniser des composants avec un systĂšme externe comme le DOM, le rĂ©seau ou un module non React. Utiliser des effets pour modifier des composants est une mauvaise pratique.

You Might Not Need an Effect


Avec un tableau de dépendance vide ([]), useEffect est appelé uniquement au montage du composant (la premiÚre fois que le composant est rendu). Cela permet de définir des effets globaux pour enregistrer des événements par exemple.

import { useEffect } from 'react';

function Listener() {
  useEffect(() => {
    const handler = () => alert("Clic !")
    window.addEventListener("click", handler)
    return () => window.removeEventListener("click", handler)
  }, [])

  return <h1>Cliquez !</h1>
}
export default Listener

Il est important de nettoyer les effets de bord lors du dĂ©montage des composants. Pour cela, dans le useEffect on peut retourner une fonction qui s’en chargera.


Ajouter une variable d’Ă©tat dans le tableau de dĂ©pendance ne va lancer l’effet de bord que quand cette variable est modifiĂ©e (et lors du montage du composant)

import { useEffect, useState } from 'react';
function Title() {
  const [title, setTitle] = useState("Default Title")

  useEffect(() => {
    document.title = title
  }, [title])

  return <input onChange={(e) => setTitle(e.target.value)}></input>
}

export default Title

Attention : Les dĂ©pendances de useEffect doivent ĂȘtre des variables d’Ă©tat (dĂ©clarĂ©es avec useState), en effet les effets ne s’applique que lors des re-rendus. Une variable classique ne dĂ©clenchera pas de re-rendu et l’effet ne sera donc pas appliquĂ©.


useEffect est l’Ă©quivalent fonctionnel des mĂ©thodes de cycle de vie des classes React.

useEffect(() => {
  //componentDidMount (s'exécute au montage du composant)
  return () => {
    //componentWillUnmount (s'exécute au démontage du composant)
  }
}, [])
useEffect(() => {
  //componentDidUpdate (s'exécute au re-rendu du composant)
  return () => {
    //componentWillUnmount (s'exécute au démontage du composant)
  }
}, [dependency])

Using React’s useEffect Hook with lifecycle methods


Le hook useMemo

Permet de mĂ©moriser une valeur et de ne la changer que lorsqu’une dĂ©pendance change. Sans useMemo cette valeur serait recalculĂ©e Ă  chaque rendu React. En gĂ©nĂ©ral on l’utilise pour mettre en mĂ©moire le rĂ©sultat de calculs lents.

Il s’utilise de la mĂȘme maniĂšre que useEffect avec une fonction de callback et un tableau de dĂ©pendance

const security = useMemo(() => {
  return passwordSecurity(password)
}, [password])

Le hook useId

Permet de gĂ©nĂ©rer un ID spĂ©cifique Ă  une instance d’un composant.

const id = useId()

Le hook useRef

Permet de conserver une variable entre les rendus sans pour autant déclencher de re-rendu.

Il s’utilise pratiquement de la mĂȘme maniĂšre que useState en mettant le rĂ©sultat dans une variable et en dĂ©finissant une valeur initiale (pas de setter en revanche). Et contrairement Ă  useState, il est mutable.

const ref = useRef(0)

Il est utile pour manipuler le DOM directement. En l’associant au paramĂštre HTML de React ref, on peut accĂ©der au nƓud DOM correspondant via le champ nodeRef.content.

import { useRef } from 'react'
export default function Content() {
  const nodeRef = useRef(null)
  const handleClick = () => ref.current.play()
  return (<>
    <video ref={nodeRef}><source src="<https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4>" type="video/mp4"></source></video>
    <button onCLick={handleClick}>Play</button>
  </>)
}

Referencing Values with Refs


Le hook useContext

Permet d’envoyer des donnĂ©es Ă  des composants enfants sans utiliser de props (problĂšme de « props drilling »).

Au préalable il faut créer un contexte grùce à la fonction React createContext. Les composants pourront alors récupérer ce contexte avec le hook useContext

export const ThemeContext = createContext("light")
const theme = useContext(ThemeContext)

DĂ©tails sur les contextes React


import { useContext } from "react"
import { ThemeContext } from "./ThemeContext"

export default function Content() {
  const theme = useContext(ThemeContext)
  return <div style={{ backgroundColor: theme === "dark" ? "#000000" : "#FFFFFF" }}>
    Content
  </div>
}
import Header from "./Header.jsx"
import Content from "./Content.jsx"
import Footer from "./Footer.jsx"
import { ThemeContext } from './ThemeContext';

export default function App() {
  return (<>
    <Header></Header>
    <ThemeContext.Provider value="dark">
      <Content></Content>
      <Footer></Footer>
    </ThemeContext.Provider>
  </>)
}
import Header from "./Header.jsx"
import Content from "./Content.jsx"
import Footer from "./Footer.jsx"
import { ThemeContext } from './ThemeContext';

export default function App() {
  return (<>
    <Header></Header>
    <ThemeContext.Provider value="dark">
      <Content></Content>
      <Footer></Footer>
    </ThemeContext.Provider>
  </>)
}

Notre composant Content contient une div qui change sa couleur de fond selon le theme fourni par le contexte. Pour cela, on utilise useContext en lui passant le nom du contexte ThemeContext.

Notre contexte ThemeContext est défini en utilisant la fonction createContext et a pour valeur par défaut light.

Enfin notre composant racine App utilise <ThemeContext.Provider value="dark">, pour passer la valeur du thĂšme Ă  ses enfants.


Les hooks personnalisés

Il est possible de crĂ©er ses propres hooks afin de crĂ©er des fonctions qui pourront ĂȘtre rĂ©utilisĂ©es dans plusieurs composants. Un hook personnalisĂ© est une fonction Javascript classique qui utilise elle-mĂȘme un hook React. Ils ne peuvent donc s’utiliser que dans des composants React comme tout hook.

function useToggle(initial) {
  const [state, setState] = useState(initial)
  const toggle = () => setState(v => !v)
  return [state, toggle]
}

La fonction useToggle est un hook personnalisé car elle utilise le hook useState de React. On peut donc la ré-utiliser dans des composants React.

export default function App() {
  const [checked, toggleCheck] = useToggle(false)
  return <>
    <input type='checkbox' checked={checked} onChange={toggleCheck}></input>
    {checked && "Je suis coché"}
  </>
}

De nombreux sites rĂ©fĂ©rencent les hooks les plus usuels Ă©crits par la communautĂ©. Ils peuvent ĂȘtre tĂ©lĂ©chargĂ©s via npm et rĂ©utilisĂ©s Ă  volontĂ©. Pour ne pas rĂ©inventer la roue, jetez-y un Ɠil :

Plus d’infos sur les hooks personnalisĂ©s


6. Route et navigation

https://grafikart.fr/tutoriels/react-router-dom-2159


React n’est qu’une simple librairie et ne gĂšre de base, ni les routes, ni la navigation entre les pages.

Pour palier Ă  ce manque, il est possible d’utiliser un framework React comme Next.js ou Remix qui supporte (entre autres) le routage. Sinon plusieurs modules communautaires existent comme TanStack Router, Wouter ou Brouther mais le plus connu reste React Router que nous allons voir ici.

npm install react-router-dom

Documentation de React Router


Comment créer des routes avec React Router ?

On commence par crĂ©er un router avec createBrowserRoute Ă  l’intĂ©rieur duquel on dĂ©finit nos diffĂ©rentes routes.

Ensuite on utilise le composant fourni par React Router <RouterProvider/> en lui précisant le router précédemment créé.

import { createBrowserRouter, RouterProvider } from "react-router-dom"

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home/>
  },
  {
    path: "/article",
    element: <Article/>
  },
])

export default function App() {
  return <RouterProvider router={router}/>
}

Ainsi on peut naviguer entre les pages / et /article en changeant l’URL dans la barre d’adresse.

Cette solution n’est Ă©videment pas idĂ©ale. C’est pourquoi afin de naviguer entre les pages grĂące Ă  des liens on peut utiliser des <Link/> ou <NavLink/> fourni par React Router.

function Home() {
  return <div>
    <h1>Accueil</h1>
    <NavLink to="/article">Voir les articles</NavLink>
  </div>
}

Barre de navigation

Pour aller plus loin et avoir une vĂ©ritable expĂ©rience « barre de navigation », c’est Ă  dire un composant contenant des liens qui va s’occuper d’afficher d’autres composants sur la page selon l’URL, il faut modifier lĂ©gĂšrement notre router en y imbriquant des routes.

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root/>,
    children: [
      {
        path: "",
        element: <Home/>
      },
      {
        path: "article",
        element: <Article/>
      }
    ]
  }
])
function Root() {
  return <>
    <nav>
        <NavLink to="/">Accueil</NavLink>
        <NavLink to="/article">Blog</NavLink>
    </nav>
    <Outlet></Outlet>
  </>
}

export default function App() {
  return <RouterProvider router={router}/>
}

Dans le router on utilise le champs children pour définir les routes enfants (on utilise des chemins relatifs). Par exemple la route /article sera un enfant de la route /.

Dans notre route parente on affiche le composant que l’on a crĂ©Ă© <Root/>, dans lequel on utilise <Outlet/>. Ce dernier s’occupe d’afficher le composant de la route enfant correspondante Ă  l’URL actuelle.


ParamĂštres passĂ©s via l’URL

On peut également faire passer des paramÚtres dans les URL, pour cela, dans la route, on précÚde le paramÚtre de : (deux-points).

{
  path: "article/:id",
  element: <ArticleDetail></ArticleDetail>
},

Puis on utilise ce paramĂštre dans le composant grĂące au hook fourni par React Router useParams.

import { useParams } from "react-router-dom"

export default function ArticleDetail() {
  const {id} = useParams()
  return <h1>Article {id}</h1>
}

Page d’erreur

Par dĂ©faut React Router affiche une page d’erreur gĂ©nĂ©rique lorsqu’il ne connaĂźt pas la route spĂ©cifiĂ©e dans l’URL mais il est possible de personnaliser ceci en utilisant le champs errorElement du router.

const router = createBrowserRouter([
{
  path: "/",
  element: <Root />,
  errorElement: <Error/>,
  children: [
    //...
  ]
}])

On rĂ©cupĂšre alors l’erreur grĂące au hook useRouteError fourni par React Router.

export default function Error() {
  const error = useRouteError()
  return <strong>{error?.error?.toString() ?? error?.toString()}</strong>
}

Récupérer des données grùce aux loaders

Un loader permet de rĂ©cupĂ©rer des donnĂ©es avant que le composant correspondant Ă  la route ne s’affiche.

{
  path: "article",
  element: <Article/>,
  loader: () => fetch("<https://jsonplaceholder.typicode.com/posts?_limit=10>")
}

React Router s’occupe de rĂ©soudre la promesse automatiquement, ce qui nous permet de mapper sur le rĂ©sultat rĂ©cupĂ©rĂ© grĂące au hook useLoaderData directement.

export default function Article() {
  const articles = useLoaderData()

  return <ul>
    {articles.map((article) => (
      <li key={article.id}>
        <NavLink to={article.id}>{article.title}</NavLink>
      </li>
    ))}
  </ul>
}

Le problĂšme avec ceci est que la page ne s’affichera que quand les donnĂ©es seront arrivĂ©es. D’un point de vue UX, ce n’est pas terrible car rien ne se passe pour l’utilisateur en attendant.

Pour amĂ©liorer ça on peut se servir du hook useNavigation qui permet de connaĂźtre l’Ă©tat du chargement et ainsi afficher un message ou une animation d’attente.

import {NavLink, Outlet, useNavigation} from "react-router-dom"
function Root() {
  const {state} = useNavigation()
  return <>
    <nav>
      <NavLink to="/">Accueil</NavLink>
      <NavLink to="/article">Articles</NavLink>
      <NavLink to="/contact">Contact</NavLink>
    </nav>
    {state === "loading" && <p>Chargement
</p>}
    <Outlet></Outlet>
  </>
}

Cependant bien qu’on ai un message de chargement, la nouvelle page ne s’affichera tout de mĂȘme que quand les donnĂ©es seront arrivĂ©es.


Il est certainement mieux d’afficher la nouvelle page Ă  l’utilisateur directement et de diffĂ©rer l’affichage des donnĂ©es. On doit pour cela rĂ©soudre la promesse nous mĂȘme en utilisant les composants Suspense et Await fournis par React et React Router ainsi que la fonction defer.

loader: () => {
  const articles = fetch("<https://jsonplaceholder.typicode.com/posts?_limit=10>").then(r => r.json())
  return defer({articles})
}

On dit à React comment résoudre la promesse et on utilise la fonction defer pour différer le résultat.

import { useLoaderData, Await } from "react-router-dom"
import { Suspense } from "react"
export default function Article() {
      const {articles} = useLoaderData()
      return <>
            <h1>Article</h1>
            <Suspense fallback="Chargement
">
                  <Await resolve={articles}>
                        {(res) => <ul>
                        //...
                        </ul>}
                  </Await>
            </Suspense>
      </>
}

<Suspense> via son prop fallback permet d’afficher un contenu de secours en attendant que ses composants enfants se chargent.

<Await> permet de récupérer les données qui ont été différées grùce à defer via son prop resolve.


Redirection

Enfin la redirection s’effecture grĂące Ă  la fonction redirect ou encore via le hook useNavigate.

redirect s’utilise dans les loaders.

import { redirect } from "react-router-dom"

{
  path: "posts",
  loader: () => redirect("/article")
}

La fonction retournĂ©e par useNavigate s’utilise Ă  l’intĂ©rieur des composants.

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

export default function Posts() {
      const navigate = useNavigate()
      useEffect(() => {
            navigate("/article")
      }, [])
}

7. Props

https://grafikart.fr/tutoriels/data-flow-react-1319


Les props sont simplement un moyen de faire passer de l’information entre composants parent et enfant.

Comment utiliser les props ?

Dans le parent

Pour passer un props Ă  un enfant, on dĂ©finit dans le composant parent des attributs personnalisĂ©s Ă  l’intĂ©rieur des balises en y associant une valeur.

export default function List() {
  const [name, setName] = useState("Alain Prost")
  const [age, setAge] = useState(68)
  const [city, setCity] = useState("Saint-Chamond")
  return <ListItem name={name} age={age} address={city}></ListItem>
}

Par exemple, ici on passe au composant enfant ListItem les props name, age et address qui contiennent respectivement nos variables name, age et city.


Dans l’enfant

Les props sont récupérés dans les paramÚtres de la fonction qui définit notre composant enfant.

export default function ListItem({ name, age, address }) {
  return <div>
    <p>Nom : { name }</p>
    <p>Age : { age }</p>
    <p>Ville : { address }</p>
  </div>

On utilise la destructuration pour récupérer les props qui nous intéressent, mais il est également possible de récupérer un objet spécial nommé props qui contiendra tous les props envoyé par le parent.

export default function ListItem(props) {
  return <div>
    <p>Nom : { props.name }</p>
    <p>Age : { props.age }</p>
    <p>Ville : { props.address }</p>
  </div>
}

Remonter une action

Il est Ă©galement possible de remonter une action utilisateur rĂ©alisĂ© dans un composant enfant vers son parent. Pour cela on peut passer en props une fonction qui agira sur l’Ă©tat du parent mais qui sera utilisĂ© par l’enfant.

export default function Watchlist() {
  const [watchedList, setWatchedList] = useState([{ id: 1, title: "Jeanne Dielman, 23, quai du commerce, 1080 Bruxelles", date: "2023-05-02" }, { id: 2, title: " Le Charme discret de la bourgeoisie", date: "2023-11-03" }])
  const addFilm = (newFilm) => {
    setWatchedList([...watchedList, newFilm])
  }
  return <Item watchedList={watchedList} onAdd={addFilm}></Item>
}
export default function Item({ watchedList, onAdd }) {
      const [filmName, setFilmName] = useState("")
      const [filmWatchedDate, setFilmWatchedDate] = useState("")
      return <>
            <ul>{watchedList.map((film) => (<li key={film.id}>{film.title} ({film.date})</li>))}</ul>
            <input type="text" value={filmName} onChange={(e) => setFilmName(e.target.value)}></input>
            <input type= "date" value={filmWatchedDate} onChange={(e) => setFilmWatchedDate(e.target.value)}></input>
            <button onClick={() => onAdd({id: crypto.randomUUID(), title: filmName, date: filmWatchedDate})}>Ajouter</button>
      </>
}

Dans le parent, on passe le props onAdd avec pour valeur une fonction addFilm qui agit sur la variable d’Ă©tat watchedList. Lors de l’appuie sur le bouton, l’enfant utilise onAdd comme s’il s’agissait de la fonction addFilm


Props spéciaux

Il existe des props spĂ©ciaux notamment deux props rĂ©servĂ©s que sont ref et key mais il existe surtout un props children qui permet d’accĂ©der aux enfants d’un composant.

Tous les Ă©lĂ©ments qui se situent entre les alises d’un composant peuvent ĂȘtre rĂ©cupĂ©rĂ©s via le props children dans ce composant.

export default function App() {
  return <ChildComponent>
    <h1>Titre</h1>
    <p>Description</p>
  </ChildComponent>
}
export default function ChildComponent({children}) {
  return <div>{children}</div>
}

C’est donc l’enfant qui s’occupera d’afficher les balises h1 et p dĂ©finie dans le parent via l’utilisation de children.

Ce props peut ĂȘtre intĂ©ressant dans le cas ou un composant ne connaĂźt pas ses enfants Ă  l’avance par exemple.


Type des props

Le passage de props d’un composant Ă  l’autre peut amener Ă  des problĂšmes de typage. C’est particuliĂšrement vrai lorsqu’on Ă©crit des composants rĂ©utilisable qui attendent des props prĂ©cis pour fonctionner.

Afin de prĂ©venir l’apparition de ce problĂšme au moment du dĂ©veloppement en facilitant le dĂ©bogage, deux solutions : utiliser un langage typĂ©e statiquement (comme TypeScript ou Flow), ou utiliser un module Javascript permettant de vĂ©rifier le type des props (npm install prop-types).

Ce module s’utilise en dehors du composant en dĂ©finissant un type pour chaque prop. Plus d’infos

import PropTypes from "prop-types"

export default function PercentageStat({ label, showAlert, score, total = 1 }) {
  return <div onMouseEnter={showAlert}><span>{label}:{ Math.round(score / total * 100) }%</span></div>
}

PercentageStat.propTypes = {
  score: PropTypes.number.isRequired,
  total: PropTypes.number,
  label: PropTypes.string,
  showAlert: PropTypes.func
}

8. Test

https://grafikart.fr/tutoriels/react-test-2158


Mise en place de l’environnement

Plusieurs bibliothĂšques de test existent, la plus connue Ă©tant Jest mais dans le cadre de Vite nous allons utiliser Vitest inclus avec ce dernier qui fournit plusieurs fonctions permettant de tester du code Javascript.

La bibliothÚque React Testing Library, quant à elle, nous permettra de tester des spécificités React comme des composants ou des hooks mais également les interactions utilisateurs.

npm install -D vitest @testing-library/react @testing-library/user-event jsdom

Il faut Ă©galement dire Ă  Vite d’utiliser l’environnement jsdom pour les tests en modifiant le fichier vite.config.js. Cet environnement est nĂ©cessaire si l’on souhaite tester des caractĂ©ristiques liĂ©es au navigateur comme le DOM. Pour exĂ©cuter des fonctions aprĂšs ou avant chaque test, on peut aussi ajouter un fichier de configuration via le champ setupFiles.

  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: "./setupTest.js"
  }
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";

afterEach(() => {
      cleanup()
})

Il faut ensuite ajouter une recette dans le package.json pour pouvoir lancer les futurs tests avec la commande npm run test.

{
  //...
  "scripts": {
    //...
    "test": "vitest"
  }
}

Tout est prĂȘt pour pouvoir tester notre application React.

On nommera nos fichiers de test de la maniĂšre suivante *.test.js ou *.test.jsx afin d’ĂȘtre dĂ©tectĂ©s par Vitest.


Tester des hooks

describe groupe des tests similaires afin d’amĂ©liorer la lisibilitĂ© des rĂ©sultats de test. On lui fournit donc une description pour expliciter ce que l’on teste.

test ou son alias it définit un test individuel et un nom pour celui-ci.

expect compare le résultat réel au résultat attendu.

import { describe, it, expect } from "vitest";
import { renderHook, act } from "@testing-library/react";

describe('useIncrement', () => {
      it('should increment value', () => {
            const { result } = renderHook(() => useIncrement({ base: 5 }))
            act(() => result.current.increment())
            expect(result.current.count).toBe(6)
      })
})

Pour rappel, un hook ne s’utilise qu’Ă  l’intĂ©rieur d’un composant, c’est lĂ  que renderHook et act fournies par RTL entrent en jeu. renderHook retourne le rĂ©sultat du hook dans un champ result.current qu’on utilise pour vĂ©rifier son bon fonctionnement. act permet d’agir sur celui-ci.


Tester des composants

Tester le rendu

Afin de tester qu’un composant s’affiche toujours de la mĂȘme maniĂšre, la mĂ©thode render provenant de RTL nous permet de gĂ©nĂ©rer l’arbre DOM de ce dernier dans un objet container qu’on peut ensuite comparer Ă  un snapshot grĂące Ă  la fonction toMatchSnapshot.

import { render, screen } from "@testing-library/react"
import { describe, it, expect } from "vitest"
import Alert from "./Alert"

describe("<Alert>", () => {
  it("should render correctly", () => {
    const { container } = render(<Alert type="danger">Erreur</Alert>)
    expect(container.firstChild).toMatchSnapshot()
  })
})

Attention : toMatchSnapshot gĂ©nĂšre une copie de l’arbre du composant concernĂ© lors de la premiĂšre exĂ©cution du test, lorsque le composant est modifiĂ© aprĂšs coup, le test ne passera plus et il faudra mettre Ă  jour le snapshot. On peut ne vĂ©rifier qu’une partie du composant en sĂ©lectionnant des enfants en particulier. Ici, par exemple, on utilise firstChild.


Tester une interaction utilisateur

La librairie RTL user-event va nous permettre de simuler des interactions utilisateurs. Par exemple userEvent.click simule un clic et prend en paramĂštre l’Ă©lĂ©ment sur lequel appuyer.

Ici on veut sĂ©lectionner un bouton « Fermer » qui dĂ©saffiche le composant. Pour ça on utilise screen qui permet de parcourir le DOM.

import { render, screen } from "@testing-library/react"
import { userEvent } from "@testing-library/user-event"
import { describe, it, expect } from "vitest"
import Alert from "./Alert"

describe("<Alert>", () => {
  it("should close the alert on click", async () => {
    const { container } = render(<Alert type="danger">Erreur</Alert>)
    await userEvent.click(screen.getByText("Fermer"))
    expect(container.firstChild).toBeNull()
  })
})

Attention : Un Ă©vĂ©nement utilisateur est une promesse et doit donc ĂȘtre attendue grĂące Ă  await.


9. Formulaires

https://grafikart.fr/tutoriels/formulaires-react-1317


En React il existe deux types de formulaires, les formulaires contrÎlés et les formulaires non-contrÎlés. Quand on parle de contrÎlé on sous entend contrÎlé par React.

Quand utiliser l’un, quand utiliser l’autre ?

Cela dépend de la situation.

Il peut ĂȘtre parfois largement suffisant d’utiliser un formulaire non contrĂŽlĂ© lorsque l’on souhaite simplement rĂ©cupĂ©rer les informations entrĂ©es par l’utilisateur lorsque celui-ci les soumet.

Par contre si on veut modifier l’interface selon ce que tape l’utilisateur ou lui renvoyer un retour en direct pour l’avertir de la force du mot de passe qu’il tape par exemple, un formulaire contrĂŽlĂ© est nĂ©cessaire.


Les formulaires non contrÎlés

Si un formulaire n’est pas contrĂŽlĂ©, il est contrĂŽlĂ© non pas par React mais par le DOM directement. Il faut le gĂ©rer comme on le ferait classiquement en Javascript. Un formulaire n’est pas gĂ©rĂ© lorsqu’il n’a pas de balise value dans ses champs d’entrĂ©e utilisateur.

export default function Form() {
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(e.target.name.value)
  }
  return <form onSubmit={handleSubmit}>
    <input type="text" name="name" defaultValue="DĂ©faut"/>
    <input type="checkbox" name="check"/>
    <button type="submit">Envoyer</button>
  </form>
}

Ici, on a donc de simples entrĂ©es utilisateur dont le rĂ©sultat sera rĂ©cupĂ©rĂ© Ă  la soumission du formulaire par l’utilisateur.


Les formulaires contrÎlés

Pour qu’un formulaire soit contrĂŽlĂ©, il faut utiliser la balise value (ou checked pour des cases Ă  cocher) associĂ© Ă  la balise onChange pour rendre le champ modifiable. Un formulaire contrĂŽlĂ© associe donc une entrĂ©e utilisateur Ă  un Ă©tat React.

export default function Form() {
  const [text, setText] = useState("")
  const [checked, setChecked] = useState(false)
  const handleChange = (e) => { setText(e.target.value)}
  const toggleCheck = () => { setChecked(!checked) }
  const handleSubmit = (e) => { e.preventDefault(); console.log("envoyé")}
  return <form onSubmit={handleSubmit}>
    <input type="text" value={text} onChange={handleChange}/>
    <input type="checkbox" checked={checked} onChange={toggleCheck} />
    <button type="submit" disabled={!checked}>Envoyer</button>
  </form>
}

Les valeurs text et checked sont donc modifiĂ©es en direct et on peut par exemple vĂ©rifier la validitĂ© d’un courriel, ou le cochage prĂ©alable d’une case.

Attention : value ne peut ĂȘtre undefined, sinon React considĂšre l’entrĂ©e comme non contrĂŽlĂ©e.


10. Style


Il existe plusieurs approches pour ajouter du style Ă  son application React.

Certaines de ces approches sont scopĂ©es, d’autres non. C’est Ă  dire que certains styles ne vont s’appliquer que dans les composants qui les importent, d’autres s’appliqueront Ă  l’application entiĂšre.

  • Faire du Inline styling en incorporant le CSS directement dans les balises JSX via le prop style (scopĂ©e mais limitĂ©e)
  • Écrire du CSS classiquement et l’utiliser via le prop className (globale)
  • Utiliser des CSS modules qui permettent de limiter la portĂ©e de ses fichiers CSS (scopĂ©e)
  • Faire du CSS in JS qui permet d’Ă©crire du style en Javascript (scopĂ©e)

Différentes approches pour styliser son app React

Scoper son CSS


Style inline

Il est définie dans le prop style de nos éléments en y fournissant un objet reprenant une syntaxe CSS.

export default function Button() {
  return <button
    style={{ backgroundColor: "#25d366", padding: "16px", color: "white" }}
  >Confirmer</button>
}

On peut faire la mĂȘme chose en utilisant des variables.

export default function Button() {
  const style = {
    backgroundColor: "#25d366",
    padding: "16px",
    color: "white"
  }
  return <button style={style}>Confirmer</button>
}

Cette approche, bien que scopĂ©e, a le gros dĂ©savantage d’ĂȘtre limitĂ©e dans le CSS que l’on peut utiliser. Impossible de faire usage de pseudo-classes par exemple.


MĂ©thode classique

Il est tout Ă  fait possible d’Ă©crire des fichiers CSS sĂ©parĂ©ment en dĂ©finissant des noms de classes.

.button {
  background-color: #25d366;      border-radius: 1rem;      border: none;
  padding: 16px;                  color: white;
}

Puis d’importer ces fichiers dans un composant et utiliser les classes grĂące au prop className.

import "./button.css"

export default function Button() {
  return <button className="button">Confirmer</button>
}

Cependant, mĂȘme s’ils ne sont pas importĂ©s dans les autres composants explicitement, les styles dĂ©finis dans button.css sont accessibles de partout (pas seulement dans notre composant <Button>). Cette approche est donc globale. La rĂ©utilisation involontaire de noms de classe pourrait ĂȘtre source d’erreurs.


Modules CSS

Les modules CSS ne sont pas une fonctionnalité native de React, il faut utiliser un outil qui les gÚrent ou en installer un. Vite par exemple gÚre les modules CSS nativement.

En gĂ©nĂ©ral, une convention de nommage permet d’utiliser ces modules CSS. Ce sont de simples fichiers CSS dont le nom se termine par *.module.css.

.button {
  background-color: #25d366;      border-radius: 1rem;      border: none;
  padding: 16px;                  color: white;
}

On importe ensuite un objet dans notre composant qui nous permet d’utiliser les classes dĂ©finies.

import classes from "./button.module.css"

export default function Button() {
  return <button className={classes.button}>Confirmer</button>
}

Nos styles sont donc limités aux fichiers qui importent et utilisent cet objet.


CSS in JS

Il existe plusieurs solutions qui permettent de faire ça, notamment styled-components que nous allons voir.

npm install styled-component

L’objet styled importĂ© permet de redĂ©finir les balises ainsi que nos propres composants en y appliquant un style par dessus.

import styled from 'styled-components';

export default function Button() {
  const StyledButton = styled.button`
    background-color: #25d366;
    padding: 16px;
    color: white;
    border: none;
    border-radius: 1rem;
  `
  return <StyledButton>Confirmer</StyledButton>
}

On rĂ©utilise alors StyledButton dans notre JSX comme s’il s’agissait d’un nouveau composant.


Quoi utiliser ?

React n’a pas d’avis sur comment nous devons styliser nos composants. La maniĂšre de faire dĂ©pend donc de notre besoin. Cependant il faut retenir plusieurs choses importantes.

Les styles inline sont moins performants que l’utilisation de classes CSS. Ils sont Ă©galement limitĂ©s dans le code CSS utilisable (pas de pseudo-classes etc.)

Certaines approches sont scopĂ©es d’autres globales. Il faut faire attention avec celles qui sont globales pour ne pas appliquer de styles involontairement.

Il est tout Ă  fait possible d’utiliser un mĂ©lange de plusieurs de ces approches prĂ©cĂ©demment citĂ©es.


Fin

Documentation · GitHub · Showcases

Illustration de l'article
comments powered by Disqus