React

Formation avancé

 

https://slides.com/b2l/code

  • Rappels React
  • React avancé
  • Redux
  • TypeScript
  • React, Redux & TypeScript

Programme

  • 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 syntax 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>

🔴 <{tag}>Gregorio Y. Zara's To Do List</{tag}>

  • soit comme attributs

src={avatar}

🔴 src="{avatar}" // la valeur de src sera {avatar}

JSX dynamic

# Décrire l'UI

Les composants React peuvent avoir des props

C'est comme les attributs en HTML

 

Le composants 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 SHOP}

Création d'une boutique en ligne

 

mise en place de la page des produits, sans dynamisme

 

npx create-react-app my-app --template typescript

https://github.com/b2l/tpreact1

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 `useXXX`

 

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 une paire de valeur : 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)) // TRUE

Ici React ne vas 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)) // FALSE
const 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 ;-)

 

https://immerjs.github.io/immer/

# Interactivité & État

Partager l'état

On a vu des composants avec des useState, mais ils étaient isolé

 

Prenons l'exemple d'un menu avec deux panel, 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 cette état comme une props

 

C'est ce qu'on appel lifting state up, c'est le 1er 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é à ces 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

Unidirectional data flow

# Interactivité & État

Unidirectional data flow

# Interactivité & État

Unidirectional data flow

# Interactivité & État

Context

La solution pour partager un état entre plusieurs enfant 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édiaire

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 utilise le context
  • Le parent fourni le context à ces descendant
# 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 fini 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écessaire
  • soit plaisante à modifier

 

Voici quelques règles pour nous aider à choisir

Comment représenter les états

# Gestion d'état

1. grouper les états liées. 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 meilleur. Cela dépend de comment on l'utilise

2. Éviter les states contradictoire, 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é

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 duplication 

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 difficile à 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 indexer par id

Single source of truth

# Gestion d'état

Il faut choisir ou 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 niveau, 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 sont parent change, que ça position dans l'arbre change, alors React va démonter le composant et détruire son state

 

Il y a des cas ou notre composant reste monté, mais ou on veut forcer le reset du state

{TP SHOP}

Création d'une boutique en ligne

 

Mise en place d'un panier

Au programme :

 

  • Refs & forwarRef()
  • 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é 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 moment ou la ref change :

- montage (quand le noeud est inséré dans le DOM)

- commit (quand le DOM est mis à jour)

Refs & forwardRef()

# React avancé

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érité du style et des contraintes de positionnement des parents.

Dans certain cas, il faut qu'un élément soit placé dans le DOM pour pouvoir faire certains calcul.


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 appeler 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() et useMemo()
  • <Suspense /> et lazy()
  • <Profiler />

Performance

# React avancé

Règle #1

Pas d'optimisation prématuré

En règle générale, React est plutôt bon côtés 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 modification)

 

On distingue les deux, parce que trop de re-render ne veux pas dire que le DOM sera modifié. Seulement que React re-calcul tout l'arbre

Performance - Globalement

# React avancé

Trop de re-render

 

React recalcul 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 et qu'on a les deux problèmes. React re-calcul tout l'arbre ET modifie le DOM.

 

La solution la plus courante à cela est la virtualization


 

Performance - Globalement

# React avancé

Pour ces problématiques, regardez du côtés de

 

 

react-windows https://github.com/bvaughn/react-window

 

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 est recalculé à chaque sélection.

Performance - Les devtools

# React avancé

On peut envisager de changer la structure pour que seul la table et l'élément dont le status à 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 recalcul 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 égale 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-calculer à 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 environnement

lazy()

# React avancé

La fonction load passer à 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 donner à fallback pendant le chargement

Si plusieurs enfant 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 chose, mais avec de grand pouvoir vient de 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épendance de notre effet.

Ce sont les variables qui invalide notre effet. Si elle change, l'effet doit être exécuter à 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écuter au premier render, et quand l'une des valeurs changent
  • Tableau vide useEffect(effect, [])
    • L'effet sera exécuter au premier render
  • Pas de tableau useEffect(effect)
    • ​​​​​​​L'effet sera exécuter à 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 sorte 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

 

https://github.com/streamich/react-use

https://usehooks.com/

https://react-hooks-library.vercel.app/getting-started

Attention !

# React avancé

useEffect() est souvent "mal" utilisé

 

Il faut distinguer deux types de logique avec React :

  • Le code pour le rendering, ou 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 change

utilisez key

Quelques exemples

# React avancé

<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'oublie de cleanup

 

  • ​​​​​​​Check si des APIs déprécié 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 SHOP}

Création d'une boutique en ligne

 

Affichage du détail des articles dans une modal

 

gestion du click outside,

chargement des articles au démarrage de l'application, https://github.com/b2l/tpreact1

{TP SHOP}

Objectifs :

- charger les données depuis le serveur

- mettre un bouton ajouter au panier sur chaque jeux

- afficher le nombre d'éléments dans le panier

- au click sur le panier afficher le détail des jeux dans une modal

- la modal doit se fermer au click sur "x" ou en cliquant hors de la modal

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 niveau et les remonter pour mettre à jour.
    • beaucoup de code non concerné à modifier en cas de changement
  • Des états qui sont éclatée 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ême, 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

  • une action est un objet
  • c'est un événement qui décrit ce qui c'est passé dans l'application (taskAdded, taskDeleted, ...)
  • Par convention elles ont un 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é par deux reducer

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 passe 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écessaire pour utiliser facilement Redux

 

Nous utiliserons uniquement redux-toolkit dans tous nos exemple

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é

Puis l'utiliser dans notre code React

 

Pour cela, nous avons 2 hooks à notre disposition :

  • useDispatch() pour dispatcher nos actions
  • useSelector(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é 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 calcul 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 long.

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 majorité des cas d'usages.

 

Mais on 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

Largement éviter parce que redux-toolkit utilise immerjs, attention aux mutation ailleurs dans votre code

Pas de side effect dans les reducers

Ils doivent être pure et pouvoir être rejouer 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 état de l'application

Les "règles"

# Gestion d'état

Les reducers doivent être "responsable" de la structure du state

ne positionné 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ées, 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 complex 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 indexer pas 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 action dispatché.

Le middleware peut ajouté des fonctionnalités à redux, comme le support d'action asynchrone, ou bloqué certaines actions, ou envoyer les actions à un serveur d'analytics, ...

 

Par défaut, redux-toolkit install 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 fourni 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 calcul 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 SHOP}

Création d'une boutique en ligne

 

Extraction de tous les états non directement lié à 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 SHOP}

Création d'une boutique en ligne

 

Chargement du catalogue avec React query,

synchronisation du panier 

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 recommande 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 toutes 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 optionnel.

Inference de type si le type n'est pas explicit

 

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é optionnel

Union types

Les types de bases

# Typescript
const a = ("12" as any) as number

Pour les cas (rares) ou il faut forcer un type

const myCanvas = document.getElementById('canvas') as HTMLCanvasElement

On spécifie avec `as`

const myCanvas = document.getElementById('canvas')
// HTMLElement

Type 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.SUCCESS
enum 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.SUCCESS
const requestStatus = 0 // Status.SUCCESS

Préferez les const enums

Les valeurs ne peuvent pas être des expressions dynamique, 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'équipes, 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 promesses
  • Partial<Type> toutes les properties optionnelles ?
  • Required<Type> toutes les properties obligatoire
  • Readonly<Type> toutes les properties readonly
  • Pick<Type, Keys> uniquement les properties sélectionné
  • Omit<Type, Keys> sans les properties sélectionné
  • NonNullable<Type> Supprime les types null et undefined
  • ReturnType<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 type 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 ces props depuis son parent, mais l'inverse

Pour Redux

# Typescript

Redux Toolkit est écrit en TypeScript.

 

Nous allons voir quelques recettes pour se faciliter la 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 typer

export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch

Attention si vous ajoutez de middleware non typer (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 bon 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 SHOP}

Création d'une boutique en ligne

 

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 simple et ne mérite pas de test unitaire

Ajouter des tests unitaires s'il y a de la logique complexe

Testing

# Testing

Préférez des test 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.
  • Tester 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/react

Mock 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())

Mise en place des tests

# Testing

{TP SHOP}

Création d'une boutique en ligne

 

Ajout de type sur l'application

Ajouter un produit dans le panier

changer la quantité

le supprimer du panier

checkout, ...

Merci

Ressource

# Testing

Formation React avancé

By b2l

Formation React avancé

  • 50