React
Formation avancée

- Rappels React
- React avancé
- Redux
- TypeScript
- React, Redux & TypeScript
Programme

Développeur web depuis 15 ans
React depuis 8 ans
J'aime les tests (j'ai écrit un Framework ! )
Agilité / Scrum
Architecture du code, code review, ...
Nicolas Medda


Nom et prénom ?
Quelle expérience avec React ?
Combien d'année de Dev ?
Qu'attendez-vous de la formation ?
Tour de table


- C'est quoi React
- Rappels
- Décrire l'interface
- Interactivité
- Gestion d'état
- Les cas aux limites
Rappel React

Crée en 2013 par Facebook
2 objectifs :
- Simplifier l'architecture des applications JavaScript
- Réduire les mutations
C'est une librairie pour gérer l'affichage des applications JavaScript
C'est quoi React
# C'est quoi React

Ce n'est pas un framework,
juste la partie Vue du MVC
Sortir du model MVC, parce qu'il ne scale pas.
React est basé sur
- une découpe en composant autonomes, responsables de leur propre état
- des vue déclaratives pour faciliter la compréhension et la maintenance
- des mises à jour réactives ultra-simples
C'est quoi React
# C'est quoi React

Voici un composant
C'est quoi React
function App() {
return (
<div>
<Header />
<MainContent />
<Sidebar />
<Footer />
</div>
)
}
function Header() {
return (
<header>
<h1>My App</h1>
<Menu />
</header>
)
}# C'est quoi React

React implémente un Virtual DOM.
C'est une representation légère de l'arbre DOM créée par notre arbre de composants.
Dès qu'une variable change (dans notre model, un "store" ou autre),
React parcours l'arbre des composants et re-calcule le Virtual DOM.
Puis il compare le Virtual DOM avec le DOM actuel, et met à jour uniquement ce qui est nécessaire
C'est quoi React
# C'est quoi React

React en 2023

# C'est quoi React
Depuis React 18 et la nouvelle documentation, les class components sont passés dans la partie "legacy" avec le warning suivant :
Depuis 2018 et l'introduction des Hooks,
il n'est plus nécessaire de créer des classes pour utiliser toutes les fonctions de React
Nous utiliserons donc uniquement la notation en fonction

Une application React est un arbre (comme en HTML) de composants.
Un composant est une fonction, il est responsable de sa propre logique, de son apparence, et de ses enfants
Ce peut être juste un bouton, ou une page entière
Rappel - Décrire l'interface
function MyButton() {
return (
<button>My button</button>
)
}# Décrire l'UI

Comme en HTML, les composants peuvent être assemblés, ordonnés et imbriqués pour créer des pages complètes
Les composants React DOIVENT commencer par une majuscule
Les composants
import { MyButton } from './button'
function App() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
)
}# Décrire l'UI

Il est recommandé de ne pas définir de composants à l'intérieur d'un autre
Attention !
function App() {
function MyButton() {
return (
<button>My button</button>
)
}
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
)
}# Décrire l'UI

Le markup utilisé est du JSX
C'est une extension de la syntaxe de JavaScript.
Le markup est transformé par babel (ou TypeScript) au moment de la compilation
JSX
// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}import React from 'react';
function App() {
return <h1>Hello World</h1>;
}# Décrire l'UI

1 - On ne doit retourner qu'un seul composants racine
Pour retourner plusieurs éléments, il faut les encapsuler dans un parent unique (<div> ou <Fragment> alias <>)
The rules of JSX
function MyApp() {
return (
<div>
<Header />
<MainContent />
<Sidebar />
<Footer />
</div>
)
}function MyApp() {
return (
<>
<Header />
<MainContent />
<Sidebar />
<Footer />
</>
)
}# Décrire l'UI

2 - Toutes les balises doivent être fermées
JSX demande à ce que toutes les balises soient explicitement fermées
The rules of JSX
<div></div>
<img /># Décrire l'UI

3 - Presque tout doit être mis en camelCase
Le JSX est transformé en JavaScript, et les noms d'attributs deviennent les clés d'objets JavaScript.
Il ne peut donc pas contenir de mot réservé (par ex. class).
Pour pouvoir être utilisé facilement, les attributs sont écrits en camelCase
Une exception à ça : les attributs aria-* et data-* (pour des raisons historiques)
The rules of JSX
# Décrire l'UI

JSX peut être pensé comme un langage de template, sauf que c'est du javascript.
On peut "interpreter" du code JavaScript dans le JSX en le mettant entre accolades {}
-
soit comme du texte à l'intérieur d'un tag JSX
<h1>{name}'s To Do List</h1>
-
soit comme attributs
<img src={avatar} />
JSX dynamic
# Décrire l'UI

Les composants React peuvent avoir des props
C'est comme les attributs en HTML
Le composant qui reçoit les props n'a pas le droit de les changer
Les props
# Décrire l'UI
function InputGroup(props) {
return (
<>
<label>{props.label}</label>
<input
type={props.type}
name={props.name}
/>
</>
)
}function App() {
return (
<InputGroup
label="Firstname"
name="firstname"
type="text" />
)
}
Les props peuvent être n'importe quel primitive JavaScript
String, boolean, Object, Array, Function, ...
Pour donner une valeur par défaut il faut utiliser la destructuration
Les props
function InputGroup({label, name, type="text"}) {
return (
<>
<label>{label}</label>
<input type={type} name={name} />
</>
)
}function InputGroup({type="text", ...otherProps}) {
const props = {...otherProps, type}
return (
<>
<label>{props.label}</label>
<input type={props.type} name={props.name} />
</>
)
}# Décrire l'UI

On peut transférer les props directement
Les props
function Profile(props) {
return (
<div className="profile">
<input {...props} />
</div>
)
}# Décrire l'UI

Les composants sont du JavaScript, on peut donc utiliser les conditions habituelles.
Un composant DOIT retourner quelque chose, du JSX ou null
Les conditions
function Profile(props) {
if (props.name === "Someone")
return null
if (props.name === "Nico") {
return (
<div className="profile">
<label>{props.name}</label>
</div>
)
}
return (
<div className="profile">
<label>{props.name} {props.isActive ? '✅' : null}</label>
<label>{props.name} {props.isActive && '✅'}</label>
<button>Delete</label>
</div>
)
}# Décrire l'UI

La méthode la plus flexible
Les conditions
function Profile(props) {
let profile = (
<>
<label>{props.name} {props.isActive ? '✅' : null}</label>
<label>{props.name} {props.isActive && '✅'}</label>
<button>Delete</label>
</>
)
if (props.name === "Someone")
profile = null
if (props.name === "Nico") {
return (
<label>{props.name}</label>
)
}
return (
<div className="profile">
{profile}
</div>
)
}# Décrire l'UI

Pour utiliser les listes directement dans le JSX, on utilise les méthodes de Array comme map() et filter()
Les listes
function PeopleList() {
const listItems = people
.filter(person => person.good)
.map(person => <li>{person}</li>)
return (
<ul>
{listItems}
</ul>
)
}# Décrire l'UI

Attention
Les éléments d'une liste doivent avoir un attribut key
C'est ce qui va permettre à React d'optimiser les re-renders quand quelque chose change
Les listes
function PeopleList() {
const listItems = people
.filter(person => person.good)
.map(person => <li key={person.id}>{person}</li>)
return (
<ul>
{listItems}
</ul>
)
}# Décrire l'UI
<ul>
<li>Harry Potter</li>
<li>Hermione Granger</li>
</ul>
Une fonction pure est une fonction qui a les caractéristiques suivantes :
- Même entrée, même sortie. Si on donne les mêmes arguments à la fonction, elle nous donnera toujours le même résultat
- Pas d'effet de bord. La fonction est isolée et ne doit pas modifier quelque chose ailleurs (changer une variable globale, faire un appel réseau, ...)
Pure components
# Décrire l'UI
function addTwo(x) {
return x + 2
}
addTwo(2) // will always return 4 and do nothing else
addTwo(3) // 5, ALWAYS
React est conçu autour de ce concept
Les composants doivent toujours retourner le même JSX pour les même props.
On ne change pas une variable externe dans un composant :
Pure components
# Décrire l'UI
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest {guest}</h2>;
}
On peut par contre créer, changer des variables locales
Pure components
# Décrire l'UI
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}

Pas d'effet secondaire (side effects)
C'est une des règles de la programmation fonctionnelle.
Mais à un moment, il doit bien y avoir un changement quelque part.
Avec React, ces sides effects doivent se passer en dehors du "rendering"
Pure components
# Décrire l'UI

Par ordre de priorité, les effets secondaires doivent être :
Pure components
# Décrire l'UI
Dans les events handler
Même s'il sont définit dans les composants, les event handlers ne sont pas exécutés pendant le "rendering", ils n'ont donc pas besoin d'être purs
Dans les useEffects
À utiliser en dernier recours !
useEffects dit à React d'exécuter le code plus tard, donc en dehors du rendering.

Bien souvent, la définition du composant, ses propriétés et son parent sont suffisants.
Éviter au maximum d'utiliser des effets secondaires dans vos composant (event handlers ou useEffect)
Une bonne pratique
# Décrire l'UI

{TP CRM}
Création d'un CRM
Mise en place de l'application, affichage des contacts, du formulaire principal et des activités


# Creation de l'application
npx create-react-app crm
cd crm
# ajout de quelques paquets
npm install react-bootstrap bootstrap
npm install --save husky lint-staged prettierimport 'bootstrap/dist/css/bootstrap.css';Dans index.js
{
"compilerOptions": {
"baseUrl": "./src",
"checkJs": true,
"jsx": "react"
}
}Créer un fichier jsconfig.json à la racine
Récupérer le fichier avec les données
https://github.com/b2l/tp_react_avance
c'est le fichier src/contacts.json
Afficher la liste des contacts
Afficher le formulair préremplie pour un contact sélectionné
Afficher les activités du contact sélectionné
Tri des activités par date
Objectifs
# TP


Il faut bien que nos applications changent quand l'utilisateur interagit avec.
En React, les données qui changent au cours du temps sont appelé state
On peut ajouter un état à n'importe quel composant et le modifier quand on le souhaite.
Rappel - Interactivité
# Interactivité & État

On peut ajouter des event handlers dans le JSX
Les composants natifs, comme button, supportent uniquement les événements natif du navigateur, comme onClick
Les composants customs, comme MyList, peuvent avoir des événements customs, comme onSelectionChange
Répondre aux événements
# Interactivité & État

Répondre aux événements
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('Playing!')}
onUploadImage={() => alert('Uploading!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<Button onClick={onPlayMovie}>
Play Movie
</Button>
<Button onClick={onUploadImage}>
Upload Image
</Button>
</div>
);
}
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
# Interactivité & État

Les hooks
# Interactivité & État
Les hooks React sont des fonctions qui permettent d'utiliser les fonctionnalités de React dans nos composants
On peut utiliser les hooks fournis par React ou les combiner pour créer les nôtres
Les hooks commencent forcement par `use`

Les hooks
# Interactivité & État
Les hooks React : https://react.dev/reference/react
Attention
On ne doit pas utiliser les hooks dans des conditions ou des branches de code. Seulement à la racine des composants :
function Item({isSelected}) {
if (isSelected)
const ref = useRef(null)
else
const ref = useRef('somethingelse')
// ERREUR
}
useState()
Pour garder en mémoire un état dans un composant, on utilise le hooks useState
Exemple d'état : la valeur d'un input, l'index de l'image à afficher dans un carousel, ...
useState retourne un tupple: l'état courant, et une fonction pour changer cet état, un setter
const [index, setIndex] = useState(0);
useState peut gérer n'importe quel primitive (string, boolean, object ...)
# Interactivité & État

useState
import {useState} from 'react'
function App() {
const [counter, setCounter] = useState(0)
function handlePlusClick() {
setCounter(counter + 1)
}
function handleMinusClick() {
setCounter(counter - 1)
}
return (
{counter}
<button onClick={handlePlusClick}>+</button>
<button onClick={handleMinusClick}>-</button>
)
}
# Interactivité & État

Render et Commit
Il y a trois phases principales pour afficher du contenu
Rendering c'est là que React parcours l'arbre des composants pour créer le Virtual DOM
Committing c'est le moment ou les modifications sont appliquées au DOM
Triggering c'est ce qui va déclencher l'affichage (changement d'une props, d'un state, animation, ...)
# Interactivité & État

State
Les states de React ne sont pas juste des variables.
Ce sont des snapshots
Utiliser le setter ne change pas immédiatement la variable, ça déclenche un re-render
console.log(counter); // 0
setCounter(counter + 1); // Request a re-render with 1
console.log(counter); // Still 0!Attention a bien utiliser le setter !
Et a utiliser const pour créer le state :
const [counter, setCounter] = useState(0)
# Interactivité & État

setState()
setState a deux signatures :
setState(value)
setState((currentValue) => newValue)
C'est important si on veut updater le state en fonction de la valeur courante et non du snapshot
# Interactivité & État

setState()
function App() {
const [counter, setCounter] = useState(0)
function increment() {
setCounter(counter + 1)
}
function incrementTimeThree() {
increment()
increment()
increment()
}
return (
<>
{counter}
<button onClick={increment}>
+1
</button>
<button onClick={incrementTimeThree}>
+3
</button>
</>
)
}function App() {
const [counter, setCounter] = useState(0)
function increment() {
setCounter(counter => counter + 1)
}
function incrementTimeThree() {
increment()
increment()
increment()
}
return (
<>
{counter}
<button onClick={increment}>
+1
</button>
<button onClick={incrementTimeThree}>
+3
</button>
</>
)
}setCounter(0 + 1)
setCounter(0 + 1)
setCounter(0 + 1)# Interactivité & État

Attention aux objets et array
Les composants étants purs, React s'appuie sur les entrées pour savoir s'il doit recalculer l'arbre Virtual DOM.
Quand React compare des states objets ou array, il compare avec Object.is()
const obj = {firstname: "Nicolas", lastname: "Medda"}
const obj2 = obj;
obj2.firstname = "Antoine"
assert(Object.is(obj, obj2)) // TRUEIci React ne va pas lancer de re-render
# Interactivité & État

Attention aux objets et array
La solution est de créer un nouvel objet ou tableau
On utiliser généralement la notation spread {...}
const obj = {firstname: "Nicolas", lastname: "Medda"}
const obj2 = {...obj};
obj2.firstname = "Antoine"
assert(Object.is(obj, obj2)) // FALSEconst a1 = ["nicolas", "harry"]
const a2 = [...a1, "marry"]
console.log(Object.is(a1, a2)) // FALSE# Interactivité & État

Attention aux objets imbriqués
C'est aussi valable pour les objets imbriqués
const harryPotter1 = {
firstname: "harry",
lastname: "potter",
picture: {
name: "l'ecole des sorciers",
src: "https://fr.web.img2.acsta.net/pictures/18/07/02/17/25/3643090.jpg",
active: true
}
}
const harryPotter2 = {
...person,
picture: {
...harryPotter1.picture,
name: "harry potter 2",
src: "http://url.com"
}
}# Interactivité & État

Exemple pour les arrays
function handleSeenClick(artworkId) {
setList(
list.map(artwork => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen };
} else {
return artwork;
}
})
);
}
function addArtwork(artwork) {
setList([...list, artwork])
}
function removeArtwork(artworkId) {
setList(
list.filter(artwork => artwork.id !== artworkId)
)
}# Interactivité & État

Pour se faciliter la vie
Il existe des librairies pour faciliter la mutation a plusieurs niveaux
A chaque équipe de faire ses choix ;-)
# Interactivité & État

Partager l'état
On a vu des composants avec des useState, mais ils étaient isolés
Prenons l'exemple d'un menu avec deux panels, qui peuvent se déplier individuellement.
On veut changer le comportement pour que l'ouverture de l'un ferme l'autre
# Interactivité & État

Partager l'état


# Interactivité & État

Partager l'état
Si on veut q'un seul Panel soit actif, alors il faut partager l'état entre les deux Panel
Pour cela, on doit supprimer l'état des composants et le déplacer dans leur parent le plus proche. Puis passer cet état comme une props
C'est ce qu'on appelle lifting state up, c'est le 1ep pattern pour partager l'état
# Interactivité & État

function Accordion() {
return (
<>
<Panel title="Panel 1">Content of panel 1</Panel>
<Panel title="Panel 2">Content of panel 2</Panel>
</>
);
}
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<div>
<h2>{title}</h2>
{isActive ? children : <button onClick={setIsActive(true)}>show</button>}
</div>
);
}
# Interactivité & État

function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
title="Panel 1"
>
Content of panel 1
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
title="Panel 2"
>
Content of panel 2
</Panel>
</>
);
}
function Panel({ isActive, onShow, title, children }) {
return (
<div>
<h2>{title}</h2>
{isActive ? children : <button onClick={onShow}>show</button>}
</div>
);
}
# Interactivité & État

Unidirectional data flow
C'est un principe de base de React.
La source de vérité est dans le state, elle est passée à ses enfants via des props, elle change par une interaction (utilisateur ou machine) au niveau du propriétaire du state
# Interactivité & État

Unidirectional data flow

# Interactivité & État

Unidirectional data flow

# Interactivité & État

Un composant est responsable de son propre état
Unidirectional data flow

# Interactivité & État

C'est <App /> qui est responsable des tâches
Donc c'est lui qui fournit la fonction pour en ajouter une
Unidirectional data flow

# Interactivité & État

Unidirectional data flow

# Interactivité & État

Context
La solution pour partager un état entre plusieurs enfants est de remonter cet état dans le parent commun, c'est le concept de Lifting state up
Mais quand les enfants sont loin dans l'arbre, on doit faire passer la props d'état et la fonction de mise à jour à tous les composants intermédiaires
Il y a beaucoup de code non concerné à modifier en cas de changement
Pour palier à cela, on utilise createContext et useContext
# Interactivité & État

Context
Le context React, c'est un moyen pour un parent de donner accès à des données à l'entièreté de ces enfants
Il y a trois étapes
- Créer le context
- Les enfants utilisent le context
- Le parent fournit le context à ses descendants
# Interactivité & État

Context
# Interactivité & État
# ThemeContext.js
import { createContext } from 'react'
export const defaultTheme = {}
export const ThemeContext = createContext(defaultTheme)
# App.js
import { ThemeContext } from './ThemeContext'
export function App() {
return (
<ThemeContext.Provider value={defaultTheme}>
<Header>
<Menu>
<Link />
</Menu>
</Header>
</ThemeContext.Provider>
)
}
# Link.js
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'
export function Link() {
const theme = useContext(ThemeContext)
return (
<a href="" className={theme.linkClass}>link</a>
)
}
Context
Un exemple intéressant de la doc React : ici
# Interactivité & État

Il y a 2 choses qui peuvent faire changer l'état de notre application :
- Une action utilisateur, clic sur un button, remplir un champ texte, clic sur un lien.
- Un événement "machine", comme une réponse réseau, un timer qui se déclenche, une image qui finit de charger
Gestion des états
# Gestion d'état

Avec React, on décrit l'interface pour les différents états
(par opposition a l'imperative programming)
Gestion des états
# Gestion d'état
async function handleFormSubmit(e) {
// Comme une recette ! 👨🍳
e.preventDefault();
disable(textarea);
disable(button);
show(loadingMessage);
hide(errorMessage);
}async function handleFormSubmit(e) {
// On place notre application dans un état
e.preventDefault();
setStatus('submiting')
setError(null)
}C'est l'interface qui s'adapte à l'état, et non pas nous qui changeons l'interface
Imperatif
Déclaratif

Il faut choisir une structure de données qui :
- représente tous les états nécessaires
- soit plaisante à modifier
Voici quelques règles pour nous aider à choisir
Comment représenter les états
# Gestion d'état

1. grouper les états liés. Si 2 states ou plus sont toujours mis à jour ensemble, rassemblez les
Comment représenter les états
# Gestion d'état
const [x, setX] = useState(0);
const [y, setY] = useState(0);const [position, setPosition] = useState({x: 0, y: 0});C'est une question de choix, la 2ème option n'est pas forcément meilleure. Cela dépend de comment on l'utilise

2. Éviter les states contradictoires, si deux états ne doivent pas être à true en même temps par ex, utiliser plutôt un état status
Comment représenter les états
# Gestion d'état
const [isSending, setIsSending] = useState(false);
const [isSend, setIsSent] = useState(false);
function handleSubmit() {
setIsSending(true);
setIsSent(false);
fetch(url)
.then(() => {
setIsSending(false);
// Ouuups on a oublié de changer isSent 🤦
})
}
const [status, setStatus] = useState('typing')
// Status est un dictionnaire, il peut être 'typing' | 'sending' | 'sent'
// On peut renforcer encore cela avec TypeScript
function handleSubmit() {
setStatus('sending')
fetch(url).then(() => setStatus('sent'))
}

3. Éviter la redondance, on utilise souvent des states pour des valeurs qui sont en fait calculées
Comment représenter les états
# Gestion d'état
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
// fullname n'est qu'une computed value
// il sera automatiquement re-calculer si l'un des state change
4. Éviter les duplications
Comment représenter les états
# Gestion d'état
const initialItems = [
{id: 0, title: "item 1"},
{id: 1, title: "item 2"},
{id: 2, title: "item 3"}
]
const [items, setItems] = useState(items)
const [selectedItem, setSelectedItem] = useState(initialItems[0])
// Ici, le selectedItem est une duplication de l'item[0]const initialItems = [
{id: 0, title: "item 1"},
{id: 1, title: "item 2"},
{id: 2, title: "item 3"}
]
const [items, setItems] = useState(items)
const [selectedId, setSelectedId] = useState(0)
// Pas de duplication, on utilise juste l'id de l'élément
// On pourrait aussi utiliser l'index
5. Éviter les arbres à plusieurs niveaux
Ils sont difficiles à mettre à jour
Comment représenter les états
# Gestion d'état
const initialState = [
{
id: 0,
firstName: 'nicolas',
comments: [
{
id: 0,
comment: 'un jolie commentaire',
read: false,
likes: [
{
id: 0,
user: 3,
},
],
},
],
},
]
Comment représenter les états
# Gestion d'état
function unlike(likeId) {
setState(
initialState.map((user) =>
userHasLike(user, likeId)
? {
...user,
comments: comment.map((comment) =>
commentHasLike(comment, likeId)
? {
...comment,
likes: comment.like.filter((like) => like.id === likeID),
}
: comment
),
}
: user
)
)
}
function commentHasLike(comment, likeId) {
return comment.likes.some((like) => likeId === like.id)
}
function userHasLike(user, likeId) {
return user.comments.some((comment) => commentHasLike(comment, likeId))
}
Comment représenter les états
# Gestion d'état
const initialUsers = [
{
id: 0,
firstName: 'nicolas',
comments: [0],
},
]
const initialComments = [
{
id: 0,
comment: 'un jolie commentaire',
read: false,
likes: [0, 3],
},
]
const initialLikes = [
{
id: 0,
user: 3,
},
{
id: 3,
user: 2,
},
]Préférez un arbre à plat

Comment représenter les états
# Gestion d'état
Préférez un arbre à plat
function unlike(likeId) {
// On met à jour les likes d'un côtés
setLikes(likes.filter((like) => like.id === likeId))
// On met à jour les commentaires pour supprimer
// le like de l'autre
setComments(
comments.map((comment) =>
// Si c'est le commentaire du like
comment.likes.include(likeId)
? // Alors on supprime ce like des likes
comment.likes.filter((like) => like !== likeId)
: // Sinon on retourne le commentaire non concerné
comment
)
)
}

Comment représenter les états
# Gestion d'état
const peopleById = {
0: {
id: 0,
firstName: "nicolas",
comments: [0],
},
3: {
id: 3,
firstName: "Harry",
comments: [1, 4, 8],
},
};
// L'accès a notre item est très facile
function getPeopleById(id) {
return peopleById[id];
}
On peut aussi envisager un objet indexé par id

Single source of truth
# Gestion d'état
Il faut choisir où placer l'état
Une règle est de le mettre le plus bas possible dans l'arbre.
Cela évite de devoir le faire passer par des props sur plusieurs niveaux, et de remonter pour modifier l'état.
Le composant qui porte l'état en est le propriétaire. C'est l'idée de "single source of truth".
Si on veut connaitre cet état, c'est dans CE composant qu'il faut regarder
Il ne faut pas dupliquer cet état dans les composants enfants

Reset du State
# Gestion d'état
React gardera le state d'un composant tant que celui ci est monté (dans le DOM)
Si son parent change, que sa position dans l'arbre change, alors React va démonter le composant et détruire son state
Il y a des cas où notre composant reste monté, mais ou on veut forcer le reset du state

{TP CRM}
Création d'un CRM
Sélection d'un contat
Édition d'un contact,
Ajout d'un contact

Gérer la sélection d'un contact
Sauvegarde du formulaire d'édition d'un contact
Ajout d'un contact
Objectifs
# TP


Pour une bonne UX des formulaires il faut :
- valider les champs avant soumission
- afficher des messages pertinents
- ne pas afficher des erreurs partout dès que le premier champ est "touché"
Gestion des formulaires
# Formulaires

Implémenter tout ça est complexe.
Il existe des librairies pour cela.
Aujourd'hui avec les hooks, la plus utilisée est react hook form
Gestion des formulaires
# Formulaires

Implémenter tout ça est complexe.
Il existe des librairies pour cela.
Aujourd'hui avec les hooks, la plus utilisé est react hook form
Gestion des formulaires
# Formulaires

Le hook principal : useForm()
React Hook Form
# TP

import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
const onSubmit = data => console.log(data);
console.log(watch("example")); // watch input value by passing the name of it
return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue="test" {...register("example")} />
{/* include validation with required or other standard HTML validation rules */}
<input {...register("exampleRequired", { required: true })} />
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>This field is required</span>}
<input type="submit" />
</form>
);
}{TP CRM}
Création d'un CRM
Utilisez React Hook Form pour gérer le formulaire

Utilisez React Hook Form
Rendre un ou plusieurs champs obligatoires
Afficher les messages d'erreurs
Objectifs
# TP

npm install react-hook-form
- Les liens sont un des fondements du web
- Ils contiennent une part de l'état de notre application :
- localhost:3000/todos/all
- localhost:3000/todos/completed
Routage
# Routing


Implémenter un router complétement fonctionnel est complex
Utilisez une librairie : React Router
Routage
# Routing

Implémenter un router complétement fonctionnel est complex
Utilisez une librairie : React Router
React Router - mise en place
# Routing

npm install react-router-domLive coding
React Router - mise en place
# Routing

Au programme :
- Refs & forwardRef()
- useId()
- ReactDOM.createPortal()
- useLayoutEffect()
- useSyncExternalStore()
- Performance
React avancé
# React avancé

Quand on doit gérer le focus des éléments, le scroll ou des APIs du DOM qui ne sont pas supportés par React, on utilise les ref
Ex classique : donner le focus à un champ texte
On ne peut pas écrire :
Refs & forwardRef()
# React avancé
function Form() {
return (
<div>
<input type="text" name="firstname" focus /> // NOPE
<input type="text" name="lastname" />
</div>
)
}
ref contient maintenant une propriété current qui nous retourne le vrai noeud DOM
React se charge de faire pointer current vers le bon noeud
Il y a seulement deux moments où la ref change :
- montage (quand le noeud est inséré dans le DOM)
- commit (quand le DOM est mis à jour)
Refs & forwardRef()
# React avancé

Des exemples :
Refs & forwardRef()
# React avancé

Ref est une propriété pour tous les éléments du Virtual DOM.
Si on a un composant custom et que l'on veut donner la possibilité d'accéder à un nœud DOM directement, alors il faut utiliser forwardRef()
Refs & forwardRef()
# React avancé
return (
<MyInput ref={inputRef} />
)
const MyInput = forwardRef((props, ref) => {
return (
<input ref={ref} {...props} />
)
})
Attention, si on modifie le DOM, alors il y a un risque de conflit avec la représentation React
Utiliser la ref (ref.current.value = 'toto'), ne provoque pas de re-render
Refs & forwardRef()
# React avancé

Génère un uuid
useId ne doit pas être utilisé pour les keys dans une liste
Utile pour générer les ids d'éléments
useId()
# React avancé
const nameFieldId = useId()
const nameFieldHintId = useId()
return (
<>
<input name="name" id={nameFieldId} aria-describedby={nameFieldHintId} />
<label htmlFor={nameFieldId}>Name</label>
<p id={nameFieldHintId}>
Description du champs
</p>
</>
)
Permet de monter un composant ailleurs dans le DOM
ReactDOM.createPortal()
# React avancé
<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>L'enfant est toujours géré par React, au sein de l'arbre dans lequel il est déclaré.
Le cas typique est la création de modal.
Ça permet aussi de ne pas hériter du style et des contraintes de positionnement des parents.

Dans certains cas, il faut qu'un élément soit placé dans le DOM pour pouvoir faire certains calculs.
C'est souvent lié au positionnement et à la taille de l'élément
Par ex, pour un tooltip : on le place au dessus, sauf s'il n'y a pas la place
useLayoutEffect() est appelé avant que le navigateur ne fasse le "paint"
Le repositionnement de l'élément ne provoquera pas de flick désagréable
useLayoutEffect()
# React avancé

useLayoutEffect()
# React avancé
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...
}
Pour synchroniser avec une source "externe"
Externe étant non React
Ex :
- être "notifié" si le navigateur est offline
- un système de store avec subscription
- un système de notification partagé
useSyncExternalStore()
# React avancé

La signature :
useSyncExternalStore(subscribe, getSnapshot)
subscribe est une fonction qui s'abonne au store et retourne une fonction pour se désabonner
la fonction prend un argument, callback, qui doit être appelé quand le store change
getSnapshot est une fonction pour lire les données depuis le store
useSyncExternalStore()
# React avancé

useSyncExternalStore()
# React avancé
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine
}
function App() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
}
Il est conseillé d'extraire la logique dans un custom hooks,
useOnlineStatus()
On peut donner un troisième argument pour le Server Side Rendering, c'est la même fonction que getSnapshot, mais pour le serveur
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
Pour les perfs, subscribe doit être défini hors du composant, et getSnapshot doit retourner un objet immutable (même référence)
useSyncExternalStore()
# React avancé

- Globalement
- dev tools
useCallback()memo()etuseMemo()<Suspense />etlazy()<Profiler />
Performance
# React avancé

Règle #1
Pas d'optimisation prématurée
En règle générale, React est plutôt bon côté performance
Performance - Globalement
# React avancé

Les problèmes de perf avec React peuvent venir de deux choses
- trop de re-render quand il y a un changement
- ralentissement au niveau du DOM (trop de noeud, trop de modifications)
On distingue les deux, parce que trop de re-render ne veut pas dire que le DOM sera modifié. Seulement que React re-calcule tout l'arbre
Performance - Globalement
# React avancé

Trop de re-render
React recalcule un composant dès que son parent change.
Comme il s'agit d'arbre, on voit que le parcours et le calcul peut vite devenir conséquent.
Pour voir les re-renders, on utilise les dev tools React.
Performance - Globalement
# React avancé


Trop de re-render
On va donc identifier les composants qui ne doivent pas changer en réponse a un changement d'état du parent
On se focus sur une branche, pas sur un composant de fin de branche (comme un bouton), parce que le gain est faible.
Performance - Globalement
# React avancé

Ralentissement DOM
Un cas classique est une trèèèès longue liste d'éléments, ou un tableau avec beaucoup de ligne (genre Excel)
Le problème quand on utilise React est qu'on a les deux problèmes. React re-calcule tout l'arbre ET modifie le DOM.
La solution la plus courante à cela est la virtualization
Performance - Globalement
# React avancé

Performance - Globalement
# React avancé

Pour identifier ou est notre problème de perfs, on s'appuie sur les devtools
D'abord avec l'outil des performances
Performance - Les devtools
# React avancé
On enregistre le moment qui nous intéresse,
soit le chargement de la page,
soit une action


Performance - Les devtools
# React avancé

On arrête l'enregistrement et on analyse le graph
On cherche les grandes ligne sans "enfants"

Performance - Les devtools
# React avancé
- S'il s'agit d'une phase du navigateur :
Style -> Layout -> Paint -> Composite
Alors il faut comprendre pourquoi on affiche / modifie autant d'élément dans le DOM
- S'il s'agit d'une de nos fonctions, alors on sait ou optimiser

Performance - Les devtools
# React avancé
Exploration d'une petite app React ensemble

Les devtools React
# React avancé
Les React dev tools ici sur le chrome dev store
Quand le problème est vraiment avec l'interactivité de notre application (après chargement)
Les dev tools React ajoutent 2 onglets au Dev Tools
- Components
- Profiler

Performance - Les devtools
# React avancé
Il faut commencer par modifier les paramètres :




Performance - Les devtools
# React avancé

On voit que toutes les lignes et tout le tableau sont recalculés à chaque sélection.

Performance - Les devtools
# React avancé

On peut envisager de changer la structure pour que seuls la table et l'élément dont le statut à changer soit recalculé.

Performance
# React avancé
Les outils que React nous donne pour optimiser les perfs :
- useCallback()
- memo et useMemo()
- Suspense & lazy()

memo()
# React avancé
Par défaut React recalcule tous les enfants d'un composants lorsque celui change (de son propre fait ou de son parent)
React nous propose memo() pour ne pas recalculer un composant si ses props n'ont pas changé
C'est le concept de memoization
Attention, React compare si les props sont égales avec Object.is mais :
Object.is(3, 3) => true
Object.is({}, {}) => false

memo()
# React avancé
Exemple :
function App() {
const products = []
return (
<>
{products.map(product => (
<Product key={product.id} product={product} />
))}
</>
)
}
const Product = memo(function Product({product}) {
return (
<div>
{/* ... */}
</div>
)
})
useMemo()
# React avancé
Pour memoizer un calcul, il y a useMemo()
import products from './products'
function App() {
const [filter, setFilter] = useState('available')
const visibleProducts = useMemo(
() => products.filter(p => p.status === filter),
[products, filter]
)
}
useCallback()
# React avancé
Dans cette exemple, même si <Product /> est memoizer avec memo(),
il sera re-calculé à chaque fois, parce handleClick sera différent
Object.is(handleClick, handleClick) => false
function App() {
const [cart, setCart] = useState([])
const handleClick = (product) => { setCart([...cart, product.id]) }
return (
<>
{products.map(product => (
<Product
key={product.id}
handleClick={handleClick}
product={product} />
))}
</>
)
}
useCallback()
# React avancé
La solution : useCallback()
function App() {
const [cart, setCart] = useState([])
const handleClick = useCallback((product) => {
setCart((currentCart) => [...currentCart, product.id])
}, [])
return (
<>
{products.map(product => (
<Product
key={product.id}
handleClick={handleClick}
product={product} />
))}
</>
)
}
useCallback()
# React avancé
const cachedFunc = useCallback(func, dependencies)

Suspense & lazy()
# React avancé
lazy(load) permet de charger du code de manière asynchrone
const MyPage = lazy(() => import('./mypage.js'))principalement utilisé pour charger dynamiquement des composants.
! Attention ! l'import dynamic ne fonctionne pas dans tous les environnements

lazy()
# React avancé
La fonction load passée à lazy doit retourner une promesse
La promesse doit retourner un composant React
const MyPage = lazy(...)
Si on essaye d'afficher le composant <MyPage />, React suspendra le render tant que MyPage n'est pas chargé

Suspense & lazy()
# React avancé
lazy() est généralement utilisé avec <Suspense />
const MyPage = lazy(() => import('./MyPage.js'))
function App() {
return (
<main>
<Header>
<Suspense fallback={'Loading...'}>
<MyPage />
</Suspense>
</main>
)
}

Suspense & lazy()
# React avancé
<Suspense /> détectera dans son arbre d'enfant si l'un d'eux est lazy.
Si un de ces enfants est lazy, il affichera le composant donné à fallback pendant le chargement
Si plusieurs enfants sont lazy, il attendra qu'ils soient tous chargés

<Profiler />
# React avancé
!!!! A utiliser uniquement quand on cherche les problèmes de perf
function onRender(id, phase, actualDuration, baseDuration,
startTime, commitTime) {
console.group(phase)
console.log(`id ${id}`)
console.log(`actualDuration ${actualDuration}ms`)
console.log(`baseDuration ${baseDuration}ms`)
console.log(`startTime ${startTime}`)
console.log(`commitTime ${commitTime}`)
console.groupEnd(phase)
}
return (
<Profiler id="products" onRender={onRender}>
<Products handleAddToCart={handleAddToCart} />
</Profiler>
)

<Profiler />
# React avancé
Le nombre de actual duration doit être vraiment plus petit entre la phase mount et les phases update.
C'est ce qui indique le temps que met React à parcourir l'arbre.
C'est aussi un moyen de voir si l'utilisation de memo() fonctionne correctement : si le nombre ne diminue pas, alors les props ne sont pas "égales"

<Profiler />
# React avancé

- L'un des hooks les plus important de React
- permet de synchroniser un composant avec un système externe
C'est un hook qui nous laisse faire beaucoup de choses, mais avec un grand pouvoir vient une grande responsabilité
C'est un point de bascule pour l'architecture de notre application
useEffects()
# React avancé

useEffect(setup, dependencies)
setup:
une fonction qui se charge de l'effet, elle peux retourne une fonction de cleanup (se déconnecter d'un système par ex)
dependencies:
un array des dépendances de notre effet.
Ce sont les variables qui invalident notre effet. Si elles changent, l'effet doit être exécuté à nouveau
useEffects()
# React avancé

useEffects()
# React avancé
function Products({ handleAddToCart }) {
const [products, setProducts] = useState([])
useEffect(() => {
fetchProducts().then(setProducts)
}, [])
return (
<>...</>
)
}
dependencies (cycle de vie)
# React avancé
Le comportement de React est différent si on passe un tableau vide, un tableau avec des valeurs, ou pas de tableau du tout
- Tableau avec valeurs
useEffect(effect, [a, b, c])- L'effet sera exécuté au premier render, et quand l'une des valeurs change
- Tableau vide
useEffect(effect, [])- L'effet sera exécuté au premier render
- Pas de tableau
useEffect(effect)- L'effet sera exécuté à chaque render

exemples
# React avancé
function Cart() {
// Should we display the cart detail, changes on hover or click
const [showDetail, setShowDetail] = useState(false)
// Handle click outside
useEffect(
() => {
const listener = (event) => {
// if the click target wasn't a child of <Cart />
if (ref.current?.contains(event.target)) return
// Hide the cart detail
setShowDetail(false)
}
// Attach our listener directly to document to capture all clicks
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
// On unmount, or re-render, remove the previous listener
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
},
// The effect should be run again if `ref` changes
[ref]
)
return (<>...</>)
}
exemples
# React avancé
function useClickOutside(ref, callback) {
useEffect(
() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) return
callback(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
},
[ref, callback]
)
}
function Cart() {
const [showDetail, setShowDetail] = useState(false)
const ref = useRef(null)
useOnClickOutside(ref, () => setShowDetail(false))
return (<div ref={ref}>...</div>)
}Créer votre propre hooks pour extraire la complexité

exemples
# React avancé
On peut imaginer toutes sortes de hooks :
Charger des données
Contrôler une carte non React
Contrôler un lecteur video
Analytics (enregistrer les clics, ...)
S'abonner / se connecter à un service externe (une websocket, une api du navigateur...)
Manipuler le DOM hors de React

Attention !
# React avancé
useEffect() est souvent "mal" utilisé
Il faut distinguer deux types de logiques avec React :
-
Le code pour le rendering, où on manipule des variables, pour in fine sortir du JSX
-
Les event handlers, c'est le code "impératif" (dans nos composants) qui fait des choses. C'est là qu'on met la plupart des side effects de notre app

Attention !
# React avancé
Les effets ne doivent servir que pour les side effects dans la partie rendering !
Ce qui est déclenché par event doit être traité dans l'event handler. C'est une zone qui est déjà hors du cycle de vie des composants React

Quelques exemples
# React avancé
N'utilisez pas d'effet pour mettre à jour un état basé sur des props ou states
const [firstname, setFirstname] = useState('')
const [lastname, setLastname] = useState('')
const [fullname, setFullname] = useState('')
useEffect(
() => setFullname(`${firstname} ${lastname}`)),
[firstname, lastname]
)const [firstname, setFirstname] = useState('')
const [lastname, setLastname] = useState('')
// Re-calculer à chaque re-render, donc quand les states changent
const fullname = `${firstname} ${lastname}`
On peut les calculer directement dans le "render"

Quelques exemples
# React avancé
N'utilisez pas d'effet pour mettre un calcul en cache
function TodoList({todos, filter}) {
const [visibleTodos, setVisibleTodos] = useState([])
// visibleTodos et redondant, il ne sert qu'a mettre en cache la version filtré
useEffect(
() => setVisibleTodos(getFilteredTodos(todos, filter)),
[todos, filter]
)
}function TodoList({todos, filter}) {
// visibleTodos ne sera calculé que si todos ou filter change
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter)),
[todos, filter]
)
}utilisez useMemo()

Quelques exemples
# React avancé
N'utilisez pas d'effet pour reset le state quand une prop change
function Post({postId}) {
const [newComment, setNewComment] = useState('')
// Quand on change de post, on force le reset du champs comment
useEffect(
() => setNewComment(''),
[postId]
)
}function PostPage({postId}) {
return (
<Post key={postId} postId={postId} />
)
}
// le state de Post change si la key changeutilisez key

Quelques exemples
# React avancé
Éviter les enchaînements de calcul
https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations

<StrictMode />
# React avancé
il est recommandé d'utiliser <StrictMode /> en développement
C'est un composant qu'on met à la racine de notre arbre React
function App() {
return (
<StrictMode>
<div>
<Header />
<Page />
<Footer />
</div>
</StrictMode>
)
}
<StrictMode />
# React avancé
En développement, <StrictMode /> va :
-
faire un 2ème re-render des composants
- aide à trouver des bugs si le rendering est impure
-
execute 2 fois les useEffects() (du 1er render)
- permet de trouver des bugs d'oubli de cleanup
- Check si des APIs dépréciées de React sont utilisées

Initialisation de l'app
# React avancé
Attention à useEffect() et <StrictMode />
function App() {
useEffect(() => {
fetchData()
checkAuthToken()
}, [])
}En dev, l'effet sera exécuté 2 fois
let didInit = false
function App() {
useEffect(() => {
if (!didInit) {
didInit = true
// maintenant on est sur que
// le code ne soit exécuter qu'une fois
fetchData()
checkAuthToken()
}
}, [])
}Si le code ne doit tourner vraiment qu'une fois, ajouter une condition :

{TP CRM}
Création d'un CRM
Ajouter un indicateur online/offline dans le header
Ajouter une activités, dans une modal
Supprimer une activités avec confirmation

{TP CRM}
Création d'un CRM
Ajouter la possibilité d'avoir un reminder sur une activité (rappeler le client le 5 décembre à 9:00)
Créer un composant qui affichera une notification quand une activité est à faire

Gestion DES états
# Gestion d'état
Quand notre base de code devient conséquente, on a deux problèmes :
-
Passer les props sur beaucoup de niveaux et les remonter pour mettre à jour.
- beaucoup de code non concerné à modifier en cas de changement
-
Des états qui sont éclatés un peu partout dans l'application
- difficile d'avoir une vue d'ensemble et de garder de la cohérence.

useReducer et redux
# Gestion d'état
React n'implémentait pas initialement de solution pour gérer les états au niveau applicatif.
Mais ces créateurs ont proposé un pattern : Flux.
Il y a beaucoup d'inspiration derrière ce pattern, comme CQRS et l'event sourcing.
Ce pattern a été repris et amélioré par Dan Abramov, qui a crée Redux, une libraire de gestion d'état, indépendante des frameworks.
En React 18, le hooks useReducer a été introduit.
C'est une version plus "légère" de Redux

useReducer et redux
# Gestion d'état
D'abord useReducer, parce que les concepts sont sensiblement les mêmes, mais que useReducer est un peu plus simple et nous servirons de base pour bien comprendre Redux

Concept
# Gestion d'état
Il y a 3 éléments clés : le store, les actions et le reducer
- Store
- C'est lui qui stock l'état.
- Il permet de connaître l'état courant
- Il permet de dispatcher un événement pour modifier l'état
const store = useReducer(reducer, initialState)
store.state // retourne l'état courant
store.dispatch(action) // dispatch un événement

Concept
# Gestion d'état
les actions
- les actions sont des objets
-
ce sont des événements qui décrivent ce qui c'est passé dans l'application (
taskAdded,taskDeleted, ...) - Par convention elles ont un champ
type - Elles sont dispatché par le store
function handleTaskAdded(task) {
dispatch({
type: 'taskAdded',
task: task,
})
}
const taskAdded = {
type: 'taskAdded',
task: task
}
Une action
Le dispatch de l'action

Concept
# Gestion d'état
le reducer
- c'est une fonction qui permet de dériver le prochain état
- il reçoit l'état courant et une action, et retourne le nouvel état
- c'est une fonction pure, il ne doit pas changer l'état qui lui est donné
- aucun effet secondaire (pas d'appel réseau, pas d'asynchrone, ...)
(state, action) => newState

Concept
# Gestion d'état


Concept
# Gestion d'état


le reducer
# Gestion d'état
const initialState = [{ id: 0, title: 'task 1', completed: false }]
export function reducer(state, action) {
switch (action.type) {
case 'taskAdded': {
return [...state, action.task]
}
case 'taskDeleted': {
return state.filter((task) => task.id !== action.taskId)
}
case 'taskUpdated': {
return state.map((task) =>
task.id === action.taskId ? { ...task, ...action.task } : task
)
}
case 'taskCompleted': {
return state.map((task) =>
task.id === action.taskId ? { ...task, completed: true } : task
)
}
case 'taskUncompleted': {
return state.map((task) =>
task.id === action.taskId ? { ...task, completed: false } : task
)
}
default:
return state
}
}

Dans <App />
# Gestion d'état
function App() {
const [taskTitle, setTaskTitle] = useState('')
const [tasks, dispatch] = useReducer(taskReducer, [])
function handleTaskTitleChange(e) {
setTaskTitle(e.target.value)
}
function handleAddTask(e) {
dispatch({ type: 'taskAdded', taskTitle: taskTitle })
setTaskTitle('')
}
function handleTaskCompleted(taskId) {
dispatch({type: 'taskCompeleted', taskId})
}
}
Dans <App />
# Gestion d'état
return (
<>
<form onSubmit={handleAddTask}>
<input
type="text"
name="newTaskTitle"
value={taskTitle}
onChange={handleTaskTitleChange}
/>
<button type="submit">Ajouter</button>
</form>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<input
type="checkbox"
checked={task.completed}
id={`task-${task.id}`}
onChange={(e) => handleTaskCompleted(e, task.id)}
/>
<label htmlFor={`task-${task.id}`}>{task.title}</label>
</li>
))}
</ul>
</>
)
Pour aller plus loin
# Gestion d'état
L'implémentation dans React permet uniquement de gérer l'état
Toute l'architecture repose sur les concepts de React.
Il faut donc passer l'état et le dispatcher dans l'arbre des composants.
On peut combiner useReducer avec l'utilisation du context pour faciliter l'utilisation de notre état

Pour aller plus loin
# Gestion d'état
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
// Dans App.js
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
)

useReducer et context
# Gestion d'état
De cette manière, on peut récupérer notre état et dispatcher des actions depuis n'importe où dans l'arbre.
Cependant, à chaque fois que l'on va vouloir gérer un nouvel état, il va falloir, soit complexifier notre reducer, soit créer un nouveau reducer (et le provider).
Ce qui devient encore plus compliqué si une action doit être utilisée par deux reducers

redux
# Gestion d'état
Redux prend le relais à ce moment là.
Par principe, il ne doit y avoir qu'un seul Store.
Le store peut être divisé en Slices, c'est une branche de l'arbre d'état
Toutes les actions passent par le Store, donc toutes les Slices ont la possibilité de réagir à une action

redux
# Gestion d'état
Redux est une libraire "bas niveau", il faut écrire pas mal de code (comme avec useReducer et context) pour mettre en place toutes l'architecture.
Redux recommande aujourd'hui l'utilisation de redux-toolkit
C'est une librairie qui ajoute toutes les fonctions nécessaires pour utiliser facilement Redux
Nous utiliserons uniquement redux-toolkit dans tous nos exemples

Mise en place
# Gestion d'état
1- Création du store
// store.js
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})2- Ajout du Store à l'app
// App.js
import { store } from './app/store'
import { Provider } from 'react-redux'
function App() {
return (
<Provider store={store}>
<Header />
...
</Provider>
)
}
Mise en place
# Gestion d'état
3- creation des slices
// store.js
import { createSlice, configureStore } from '@reduxjs/toolkit'
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {
state.push(action.payload.post)
},
deletePost(state, action) {
return state.filter((post) => post.id !== action.payload.postId)
},
updatePost(state, action) {
return state.map((post) =>
post.id === action.payload.post.id ? action.payload.post : post
)
},
},
})
export const store = configureStore({
reducer: {
posts: postsSlice.reducer,
},
})

Utilisation
# Gestion d'état
Il faut créer les actions possibles dans les stores (slices).
Puis implémenter la logique de changement d'état associée
Puis l'utiliser dans notre code React
Pour cela, nous avons 2 hooks à notre disposition :
useDispatch()pour dispatcher nos actionsuseSelector(selector)pour requêter le store
Ces deux hooks viennent du package react-redux

Utilisation
# Gestion d'état
function TodoList() {
const [newTask, setNewTask] = useState('')
const visibleTodos = useSelector(getVisibleTodos)
const dispatch = useDispatch()
function handleAddNewTask(e) {
e.preventDefault()
dispatch(createTask({ title: newTask }))
setNewTask('')
}
function handleNewTaskChange(e) {
setNewTask(e.target.value)
}
function handleToggleCompleted(todo) {
return (e) => {
e.target.checked
? dispatch(taskCompleted(todo.id))
: dispatch(taskUncompleted(todo.id))
}
}
//...
}
Les selecteurs
# Gestion d'état
Le concept est important, c'est l'idée que les données dérivées ne doivent pas être stockées dans le store.
Le store doit contenir le minimum d'information possible.
Cela permet d'éviter les incohérences de données
Les listes filtrées, les sommes et autres valeurs calculées ne doivent pas être dans le store
Les sélecteurs sont des fonctions qui prennent en premier paramètre le state, et retourne la data qui nous intéresse
C'est dans ces fonctions que l'on calcule l'état dérivé.

Les selecteurs
# Gestion d'état
// Selector.js
export function getVisibleTodos(state) {
const filter = getTodosFilterFunction(state)
return filter
? Object.values(state.todosById).filter(filter)
: Object.values(state.todosById)
}
export function getTodosFilterFunction(state) {
switch (getTodosFilter(state)) {
case 'all':
return null
case 'completed':
return (todo) => todo.completed
case 'notcompleted':
return (todo) => !todo.completed
}
}
export const getTodosFilter = (state) => state.ui.todosFilter
Les selecteurs
# Gestion d'état
Les calculs dans les sélecteurs peuvent être longs.
Pour cela, redux-toolkit propose la fonction createSelector()
La fonction vient du package Reselect
Le sélecteur sera mémoïsé et ne sera recalculé que si un de ces argument change

Le tout ensemble
# Gestion d'état
Live coding d'une petite application de todos

Quand ça ne suffit pas
# Gestion d'état
Je vous ai présenté l'API la plus courante de redux-toolkit.
Elle couvre la très grande majorité des cas d'usages.
Mais on peut faire du redux "à la main" si besoin.
redux-toolkit a aussi une API pour créer uniquement les actions, les reducers.

createAction() avec prepare
# Gestion d'état
import { nanoid, createAction } from '@reduxjs/toolkit'
// L'argument prepare est optionel
const addTodo = createAction('todos/add', function prepare(title) {
// Il nous permet de customiser le payload
// C'est un niveau d'indirection en plus,
// mais il permet de decoupler le(s) composant(s) qui dispatch l'action du(des) reducer(s)
return {
payload: {
title,
id: nanoid(), // Ajout d'un id
createdAt: new Date().toISOString(), // Ajout d'un timestamp
},
}
})
const TodoSlice = createSlice({
name: 'todos',
initialState: {},
reducers: {},
extraReducers: {
// On peut écouter des actions customs dans `extraReducers`
// ici on utilise la notation dynamique,
// mais on pourrait utiliser une string ('todos/add')
[addTodo]: (state, action) => {
const {id, title, createdAt} = action
state[id] = {id, title, createdAt}
}
}
})
Bonnes pratiques
# Gestion d'état
Utilisez l'arborescence "ducks"
Rassemblez tous les éléments d'une slice ensemble
Garder les sélecteurs à part
le premier paramètre étant le state global, vos reducers n'ont pas à savoir ou ils sont dans l'arbre d'état.
utiliser createSelector, avec des combinaisons, c'est mieux pour les performances

Les "règles"
# Gestion d'état
Plus que des bonnes pratiques, se sont des règles à respecter pour que Redux fonctionne correctement et éviter les surprises
Ne pas muter le state
redux-toolkit utilise immerjs, ce qui évite grandement le risque. Attention aux mutations ailleurs dans votre code
Pas de side effect dans les reducers
Ils doivent être purs et pouvoir être rejoués plusieurs fois sans déclencher un appel réseau ou autre, pas de Math.random(), Date.now(), ...

Les "règles"
# Gestion d'état
Pas de valeur non sérialisable dans les actions ou le state
Principalement pour le debugging avec Redux DevTools, mais aussi si on veut faire de l'analytics, ou des choses poussées avec un serveur
Un seul store par App
C'est la garantie que chaque slice reçoive toutes les actions et puisse changer son état
Un maximum de logique dans les reducers
Le reducer est facilement testable, et on peut mieux y décrire les différents états de l'application

Les "règles"
# Gestion d'état
Les reducers doivent être "responsables" de la structure du state
ne positionnez pas simplement le nouvel état en fonction de payload :
taskAdded(state, action) {
state[action.payload.id] = action.payload
// Le reducer ne sait pas ce qu'il stock
}
taskAdded(state, action) {
const {id, title, completed=false} = action.payload
state[id] = {id, title, completed}
// On garanti que la structure est bien celle attendu
}
Les "règles"
# Gestion d'état
La structure du state ne doit pas reprendre celles des composants
Concevoir l'arbre d'état est une tâche en soit et demande réflexion.
Séparer le state par ce qu'est la donnée, et non pas par fonctionnalité.
{
posts,
users,
comments,
ui
}{
userList,
postsList,
loginScreen,
}{
userReducer,
postReducer
}Good
Bad
Bad

Les "règles"
# Gestion d'état
Normaliser les états complexes et les relations
{
posts: {
byId: {
'post1': {
id: 'post1',
title: '...'
author: 'user1',
comments: ['comment1', 'comment2']
},
//...
},
allIds: ['post1']
},
comments: {
byId: {
'comment1': {
id: 'comment1',
body: '...'
user: 'user3'
}
},
allIds: ['comment1']
}
}Séparer le state par type d'entités.
Chaque type d'entité est un objet
Chaque élément est indexé par son id
on peut utiliser la structure byId et allIds.
allIds permet d'ordonner les éléments

Les "règles"
# Gestion d'état
Tout ne doit pas être dans le store Redux
Un seul store par App oui, mais seulement pour stocker ce qui est global (données et/ou état)
Si un état est local (isOpen pour une modal par ex), il peut rester au niveau du composant
Connecter plus de composant au store avec useSelector()
Il vaut mieux provoquer une mise à jour localisé uniquement sur les composants qui en ont besoin.
C'est bien plus efficace pour les performances de React

Les "règles"
# Gestion d'état
Utiliser redux devtools
Disponible sur les différents store des navigateurs.
Permet de voir tout l'état à un instant T
Permet de voir toutes les actions qui sont dispatchées
Permet de revenir dans l'historique des actions pour voir les changements d'état qui en découle

Les conseils :)
# Gestion d'état
Définissez les actions comme des événements et non des setters
L'idée et de pouvoir lire le log des actions et de comprendre ce qui c'est passé dans l'application.
"users/updated" vs "users/setName"
Quand on pense en terme d'événement, on a :
moins d'actions
moins de risque de vouloir enchaîner les actions
users/setName -> users/setFullName
Utilisez des événements permet de garder la logique dans le reducer

Les conseils :)
# Gestion d'état
Exemple de la doc redux
{ type: "food/orderAdded", payload: {pizza: 1, coke: 1} }
Comme un événement
{
type: "orders/setPizzasOrdered",
payload: {
amount: getState().orders.pizza + 1,
}
}
{
type: "orders/setCokesOrdered",
payload: {
amount: getState().orders.coke + 1,
}
}Comme des setters

Les side effects
# Gestion d'état
Pour faire des appels réseau, pour d'autres choses asynchrone (connexions à un serveur, animation, ...)
Pas dans les reducers
Soit dans les composants React
Soit dans les actions creator

Les side effects
# Gestion d'état
le store redux peut avoir des "middlewares".
c'est une fonction qui s'execute pour chaque actions dispatchées.
Le middleware peut ajouter des fonctionnalités à redux, comme le support d'action asynchrone, ou bloquer certaines actions, ou envoyer les actions à un serveur d'analytics, ...
Par défaut, redux-toolkit installe le middleware redux-thunk (et d'autres choses).
Il permet le support d'action asynchrone

actions asynchrone
# Gestion d'état
Une action asynchrone doit retourner une promesse
(dispatch, getState) => {
return createTodoOnServer(title)
.then(todo => disptach(taskAdded, todo))
}const addTodo = title => (dispatch, getState) => {
return createTodoOnServer(title)
.then(todo => disptach(taskAdded, todo))
}Pour rester cohérent avec les actions creator,
on écrit des thunk actions creator, qui retourne la fonction thunk

createAsyncThunk
# Gestion d'état
c'est une fonction fournit par redux-toolkit pour faciliter la creation des thunks
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await client.get('/fakeApi/posts')
return response.data
})createAsyncThunk va automatiquement générer 3 actions :
posts/fetchPosts/pending
posts/fetchPosts/fulfilled
posts/fetchPosts/rejected

appels réseau
# Gestion d'état
Quand on fait des appels réseau, il y a plusieurs chose à ne pas oublier :
- les races conditions
- les états de chargements
- les erreurs
- le debouncing
- la pagination

appels réseau
# Gestion d'état
Pour toutes ces raisons, on recommande d'utiliser une librairie
Pour une application data-driven, de petite à moyenne taille, utilisez React Query
Pour une application plus grosse, ou il y a beaucoup de calculs sur la data pour représenter l'état de l'application, utilisez l'addon de Redux : RTK Query

appels réseau
# Gestion d'état
Je ne présenterai que React Query
Mais il faut savoir que si la philosophie ne vous plait pas, ou si ça ne correspond pas à votre besoin, il y a d'autre solution.
Je vous déconseille cependant d'essayer de tout implémenter vous même.

{TP CRM}
Création d'un CRM
Extraction de tous les états non directement liés à l'affichage dans un reducer

React Query
# React Query
React Query (TanStack query en fait) se décrit comme une libraire pour gérer l'état serveur
par opposition à Redux ou au primitive React de gestion d'état client
React Query s'utilise directement dans React, via des hooks.

useQuery()
# React Query
Le hook principal
const result = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList
})La queryKey est comme l'attribut key de React dans une liste.
Il doit être précis pour que React Query sache quand il doit relancer la requête.
function Todo({todo}) {
const todo = useQuery({
queryKey: ['todo', todo.id],
//...
})
}

useQuery()
# React Query
queryFn : n'importe quel fonction qui retourne une promesse
const fetchTodos = async () => {
const rs = await fetch('/api/todos')
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await rs.json()
return data
}queryFn doit throw pour que React Query sache qu'il y a une erreur

useQuery()
# React Query
useQuery(queryKey, queryFn, options)
Parmi les options en plus :
- networking - gestion du online/offline
- dépendance sur d'autre query - enabled
- retries
- pagination
- gestion du cache
- polling

useQuery()
# React Query
useQuery retourne un objet avec les clés suivantes :
data
dataUpdatedAt
error
errorUpdatedAt
failureCount
failureReason
isError
isFetched
isFetchedAfterMount
isFetching
isPaused
isLoading
isLoadingError
isPlaceholderData
isPreviousData
isRefetchError
isRefetching
isInitialLoading
isStale
isSuccess
refetch
remove
status
fetchStatus

useQuery()
# React Query
function App() {
const {isLoading, isError, data, error} = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const rs = await fetch('/api/todos')
const data = await rs.json()
return data
}
})
}
useMutation
# React Query
function App() {
const [taskTitle, setTaskTitle] = useState('')
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
function handleCreateTodo(e) {
e.preventDefault()
mutation.mutate(taskTitle)
setTaskTitle('')
}
if (mutation.isLoading)
return 'Loading ...'
return (
<>
{mutation.isError && <div>An error occured {error}</div>}
{mutation.isSuccess && <div>Todo added!</div>}
<form onSubmit={handleCreateTodo}>
{/* ... */}
</form>
</>
)
}
Quelques hooks à regarder
# React Query
useIsFetching : si vous voulez un loader globale. Il vous dit combien de requête sont en cours
useInfiniteQuery : designer pour la pagination avec infinite scrolling
useQueryErrorResetBoundary et <QueryErrorResetBoundary />
Permet de facilement ajouter un bouton pour relancer une requête en erreur

Example avec Todo
# React Query

{TP CRM}
Création d'un CRM
Chargement des contacts et activités avec React query,
Synchronisation des modifications

TypeScript
# Typescript
TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.

TypeScript
# Typescript
Beaucoup de projet et d'équipes recommandent d'utiliser TypeScript pour les développements front-end aujourd'hui.
La quantité de code front est devenu bien plus importante
L'architecture et l'interconnection aussi
Pour éviter toute une catégorie de bug en production, on peut utiliser TypeScript.

TypeScript
# Typescript
Typescript permet de configurer son degré de "rigueur".
Par défaut :
les types sont optionnels.
Inference de type si le type n'est pas explicite
Mode Strict: true
Active toutes les options. Les plus importantes :
noImplicitAny => erreur si l'inference renvoie any
strictNullChecks => force a explicitement traiter null et undefined

TypeScript
# Typescript
Redux :
- typer toutes les actions et leur payload et éviter de dispatcher la mauvaise action
- typer le state permet de s'assurer qu'on aura toujours la bonne structure
React :
- typer les composants permet de passer les bonnes Props
- typer les states permet de s'assurer de la cohérence lors des updates

Quelques opérateur ES6
# Typescript
Ils viennent de TypeScript même s'il font aujourd'hui parti d'ES6
- Optional chaining operator
ref.current?.focus()
if(ref.current) ref.current.focus()
- Nullish coalescing operator
setTaskTitle(e.target.value ?? 'default title')
Gère correctement les valeurs "falsy", à préférer par rapport à e.target.value || 'default title'

Les types de bases
# Typescript
const a: string = 'test'
const b: boolean = false
const x: number = 12
const c: Array<string> = ['']
const d: String[] = ['']const func = (x: number): number => x*2
function myTestFunc(arg1: string): boolean {
return arg1 === 'my test password'
}

Les types de bases
# Typescript
const person = {firsname: 'Nicolas', lastname: 'Medda'}
interface Person {firsname: string, lastname: string}function printId(id: number | string) {
// Attention à l'utilisation du coup
console.log(id.toUpperCase())
}Objects
interface Person {firsname: string, carModel?: string}
function drive(person: Person) {
person.carModel // Attention, peut être undefined
}Propriété optionnelle
Union types

Les types de bases
# Typescript
const a = ("12" as any) as numberPour les cas (rares) ou il faut forcer un type
const myCanvas = document.getElementById('canvas') as HTMLCanvasElementOn spécifie avec `as`
const myCanvas = document.getElementById('canvas')
// HTMLElementType assertions
Quand TypeScript ne peut pas connaître le type exact de retour
Par ex :

Les types de bases
# Typescript
function getStatus(): 'success' | 'error' | 'inprogress' {}
function compare(a: string, b: string) : -1 | 0 | 1 {}Literal types
une chaîne de caractère qui est une constante est un type
De même pour les nombres

Les types de bases
# Typescript
enum Status {
SUCCESS,
ERROR,
IN_PROGRESS
}
const requestStatus : Status = Status.SUCCESSenum Status {
SUCCESS = 0,
IN_PROGRESS = "IN_PROGRESS"
}Enums
Attention ! Les enums sont une vraie addition de Typescript, et non une extension de type.
On peut donner une valeur à la "clé" :

Les types de bases
# Typescript
const enum Status {
SUCCESS,
ERROR,
IN_PROGRESS
}
const requestStatus : Status = Status.SUCCESSconst requestStatus = 0 // Status.SUCCESSPréferez les const enums
Les valeurs ne peuvent pas être des expressions dynamiques, mais ils sont bien mieux en terme de performance.
Le compilateur remplacera la valeur directement dans le code et supprimera l'enum

Les types de bases
# Typescript
interface Todo {
id: number,
title: string,
completed: boolean
}
interface State {
todos: {
byId: {
[id: string]: Todo
},
allIds: Array<string>
},
ui: {
selectedTasks: Array<string>,
filter: 'all' | 'completed' | 'notcompleted'
}
}
L'inference
# Typescript
Il n'est bien souvent pas nécessaire d'écrire le type des variables
Cela reste un choix d'équipe, mais :
let name: string = "Nicolas"
ET
let name = "Nicolas"Revient exactement au même pour Typescript
Utilisez votre éditeur pour vous guider. L'intellisens de VS Code utilise TypeScript
Opinionated

Les génériques
# Typescript
Permet de définir un type qui dépend d'un autre type, quel qu'il soit
interface Action<T> {
type: string
payload: T
}
const action: Action<string> = { type: 'mon_action', payload: 'hello' }
interface State {
status: string
}
interface Store<T, A> {
getState(): T
dispatch(action: A): void
}

Manipulation des types
# Typescript
TypeScript a plein de fonctions et type utilitaires pour cela
Nos types dépendent souvent d'un autre type.
Pour éviter les bugs, il faut que le type dérivé soit dynamique

keyof
# Typescript
type Point = { x: number; y: number };
type P = keyof Point;
// Equivalent à :
type P = "x" | "y"const colors = {
white: '#f2f2f2',
black: '#333',
primary: '#c3e2f1'
}
type Colors = keyof typeof colors
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
color: Colors
}Retourne un literal (string ou number) des clés de l'objet
Un exemple, récupérer les clés d'un dictionnaire

Utilities
# Typescript
Awaited<Type>Pour les promessesPartial<Type>toutes les properties optionnelles ?Required<Type>toutes les properties obligatoiresReadonly<Type>toutes les properties readonlyPick<Type, Keys>uniquement les properties sélectionnéesOmit<Type, Keys>sans les properties sélectionnéNonNullable<Type>Supprime les typesnulletundefinedReturnType<Type>Le type de retour d'une fonction

Discrimination
# Typescript
TypeScript analyse le flux d'un programme pour réduire le type d'une variable.
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
console.log(x); // let x: boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x); // let x: string
} else {
x = 100;
console.log(x); // let x: number
}
return x; // let x: string | number
}
Discriminated Union Type
# Typescript
TypeScript peut discriminer deux (ou plus) union type en fonction du flow
type State = {connected: boolean, channel: string | null}
type Action = { kind: 'subscribed', channel: string} | { kind: 'disconnected'}
function reduce(state: State, action: Action): State {
switch (action.kind) {
case 'disconnected': {
return {...state, connected: false, channel: null}
}
case 'subscribed': {
return {...state, connected: true, channel: action.channel}
}
}
}
Discriminated Union Type
# Typescript
Reprenez cette exemple dans votre éditeur.
Voyez comment dans la branche `disconnected`, TypeScript reduit le type possible.
Si vous tapez `action.` alors vous n'aurez que `kind`.

Réduire les types possible
# Typescript
Dès que nous avons un ensemble de type possible, on dit que l'on a une Union :
function (arg: Type1 | Type2)
Quelque soit Type1 et Type2 (des classes, enums, primitive, ...)
Ce que nous avons vu est un cas "simple" de réduction du type.
Nous avons une propriété commune, ou en tout cas discriminante. On fait un test dessus (if ou switch), qui nous permet de réduire les types possibles dans la branche

Réduire les types possible
# Typescript
Pour les primitives: typeof
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding);
}
return padding + input;
}Mais ça ne suffit pas forcement
typeof null
// 'object'On peut donc aussi tester la "truthiness"
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === 'object') {
console.log(strs.join(' '))
} else if (typeof strs === 'string') {
console.log(strs)
} else {}
}
Réduire les types possible
# Typescript
Pour les classes et objets: in
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}instanceof
function printDate(date: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
Type predicate
# Typescript
Quand ces solutions ne suffisent pas, ou qu'on veut contrôler précisément le type réduit pour une branche de code, on peut créer nos propres predicate
function isFish(pet: Fish | Bird) : pet is Fish {
return (pet as Fish).swim !== undefined
}
let myPet: Fish | Bird = getSmallPet()
if (isFish(myPet)) {
// We know it's a Fish
myPet.swim()
} else {
// We know it's not a Fish, so it's a Bird
myPet.fly()
}
Assertion functions
# Typescript
De la même manière, on peut contrôler le flux avec une erreur
function assertIsString(val: any): asserts val is string {
if (typeof val !== "string") {
throw new AssertionError("Not a string!");
}
}
function yell(str: any) {
assertIsString(str);
// Now TypeScript knows that 'str' is a 'string'.
return str.toUppercase();
}
Pour React
# Typescript
Pour étendre un élément HTML
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
specialProp?: string;
}
type ButtonProps = { specialProp ?: string } & React.ComponentPropsWithoutRef<"button">Pour étendre un composant
export interface MyButton extends React.ComponentPropsWithoutRef<typeof Button> {
specialProp?: string;
}
type MyButton = React.ComponentPropsWithoutRef<typeof Button> & {
specialProp?: string;
}
Pour React
# Typescript
Un pattern pour les sous-ensembles de props
import { connect } from 'react-redux'
import { ArticleProps, Article } from './ArticleProps'
function mapStateToProps(state): Pick<ArticleProps, 'title' | 'description'> {
return {
title: selectTitle(state),
description: selectDescription(state),
}
}
export default connect(mapStateToProps)(Article)Préférez garder le composant le plus bas dans l'arbre le plus simple possible.
Ce n'est pas à l'enfant de dériver ses props depuis son parent, mais l'inverse

Pour Redux
# Typescript
Redux Toolkit est écrit en TypeScript.
Nous allons voir quelques recettes pour se faciliter le travail

Pour le State
# Typescript
On récupère le type du state avec :
export type RootState = ReturnType<typeof store.getState>On va souvent travailler par slice.
configureStore({
reducer: {
one: oneSlice.reducer,
two: twoSlice.reducer
}
})
Pour le Dispatch
# Typescript
On va exposer un hook custom pour retourner la fonction dispatch correctement typés
export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatchAttention si vous ajoutez des middleware non typés (ou mal), il vous faudra faire le travail vous même

createAction
# Typescript
Si vous utilisez createAction
export const increment = createAction<number>('increment')Et pour l'utiliser :
function test(action: Action) {
if (increment.match(action)) {
assert(action.payload === 5) // we know payload is a number here
}
}
createReducer
# Typescript
Pour avoir les bons types des payloads, il faut utiliser la notation `builder`
const increment = createAction<number, 'increment'>('increment')
const decrement = createAction<number, 'decrement'>('decrement')
createReducer(0, (builder) =>
builder
.addCase(increment, (state, action) => {
return state + action.payload
})
.addCase(decrement, (state, action) => {
return state - action.payload
})
)
createSlice
# Typescript
createSlice créé les actions pour nous. On peut donc typer les actions inline
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => {
state + action.payload
},
},
})
createSlice
# Typescript
Pour typer le State
interface State {
selected: null | string
ids: string[]
byId: {
[id: string]: Product
}
}
const initialState: State = { selected: null, ids: [], byId: {} }
createSlice({
name: 'products',
initialState,
reducers: {
select: (state, action: PayloadAction<Product>) => {
state.selected = action.payload.id
},
},
})

prepare actions
# Typescript
Si on veut préparer les actions directement dans le slice
const select = {
reducer: (state, action: PayloadAction<string>) => {
state.selected = action.payload
},
prepare: (product: Product) => {
return { payload: product.id }
}
}
const productSlice = createSlice({
name: 'product',
initialState,
reducers: {
select
}
})
createAsyncThunk
# Typescript
Pour que les types soit correctes, il faut typer les arguments de la fonction et son retour
const fetchUserById = createAsyncThunk(
'users/fetchById',
// Declare the type your function argument here:
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
// Inferred return type: Promise<MyData>
return (await response.json()) as MyData
}
)
{TP CRM}
Création d'un CRM
Ajout de type sur l'ensemble de l'app

Testing
# Testing


Testing
# Testing
La recommandation de l'équipe redux est de ne pas écrire trop de test unitaire, mais surtout des tests d'intégration
Beaucoup de reducer et d'action creator sont simples et ne méritent pas de test unitaire
Ajouter des tests unitaires s'il y a de la logique complexe

Testing
# Testing
Préférez des tests d'intégrations
-
avec un vrai
<Provider>et un vrai store pour faire le render du composant à tester - Utilisez des interactions avec l'interface, qui vont déclencher des actions et rafraichir le composant.
- Testez l'état d'affichage du composant
- Mockez les appels réseaux pour ne pas avoir à modifier le code
Concentrez votre effort sur le mock réseau, plus que sur une couverture de unit-test à 100%

Les librairies
# Testing

React testing library
permet de monter des composants React et d'interagir avec
Utilise les outils fourni par React-Dom
Jest
Inclus de base dans create-react-app, c'est lui qui va exécuter les tests

Mock service worker
permet de mocker les appels réseaux

Les librairies
# Testing

React testing library
npm install --save-dev @testing-library/reactMock service worker
npm install msw --save-dev
Setup
# Testing
Création d'un renderer réutilisable, qui instancie le store et tout ce qu'il faut
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
store = setupStore(preloadedState),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}
Test
# Testing
Test de l'application Todo
import React from 'react'
import { fireEvent, getByRole, screen } from '@testing-library/react'
import { renderWithProviders } from './test-utils'
import App from './App'
test('renders the TodoApp', async () => {
renderWithProviders(<App />)
const label = screen.getByText(/What needs doing/i)
expect(label).toBeInTheDocument()
const input = screen.getByRole('textbox', {name: /What needs doing/i})
expect(input).toBeInTheDocument()
})
Test avec mock du reseau
# Testing
export const handlers = [
rest.get('/api/todos', (req, res, ctx) => {
return res(
ctx.json(serverPayload),
ctx.delay(150)
)
}),
]
const server = setupServer(...handlers)
// Enable API mocking before tests.
beforeAll(() => server.listen())
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
{TP CRM}
Création d'un CRM
Ajouter des tests

Ajouter des tests pour :
- la navigation
- l'édition d'un contact
- l'ajout d'un contact
- l'ajout d'une activités
- la notification d'une activités plannifié
Objectifs
# TP

React avancé pour Retengr
By b2l
React avancé pour Retengr
- 26