Explorez React : Introduction des fondamentaux
Plan
Les concepts les plus importants de React
- đ *Introduction** - prĂ©sentation globale de la librairie React
- âšïž *Installation et CLI** - comment installer et utiliser la CLI
- đ§âđ» *Le langage JSX** - introduction au langage JSX
- âïž *Composants** - l’architecture dĂ©coupĂ©e en composants
- đïž *Hooks** - le state, les effets
- đŁïž *Route et navigation** - navigation entre les pages
- đŁïž *Props** - communication entre les composants
- đ©ș *Test** - faire des tests sur son appli React
- âïž *Formulaires** - gĂ©rer les interactions utilisateurs
- đ *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.jsx
et 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.jsx
constitue 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
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.
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>
</>)
}
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
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
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