React

Une bibliothèque JavaScript pour créer des interfaces utilisateurs

Formateur: Fabio Ginja
@Ambient-IT

Introduction à React

Multi Page Application

Chaque changement dans notre page web force un rafraîchissement de la page, et donc le chargement d'une nouvelle page html. Application centrée serveur.

Single Page Application

Chaque changement provoque un appel AJAX au serveur. Ce dernier renvoie des données et ne me à jour que certaines parties de la page. Application centrée client.

Frameworks Javascript

  • Vue.js - Alibaba, Tencent, Baidu
  • Angular - Forbes, Xbox, Parrot
  • React - Facebook, Netflix, Airbnb, Slack
  • Svelte - NY Times
  • Ember.js - Linkedin
  • Polymer - Youtube, Google Play Music, Coca-Cola
  • Backbone.js - Reddit, Amazon
  • Aurelia - seloger
  • Meteor - SFR
  • Mithril - Vimeo, Nike

Les trois frameworks les plus populaires restent Angular, React et Vue.js.

React - À base de composants

"Définissez des vues simples pour chaque état de votre application, et lorsque vos données changeront, React mettra à jour, de façon optimale, juste les composants qui en auront besoin."

"Créez des composants autonomes qui maintiennent leur propre état, puis assemblez-les pour créer des interfaces utilisateurs complexes."

Exemple de composant

Un Component décrit l'HTML qui sera produit en fonction des paramètres qui lui auront été fournis.

Lorsqu'un des paramètres change (name ou description), l'HTML sera mis à jour automatiquement.

import React from 'react'

const Card = ({name, description}) => {
  return (
    <div>
      <span>Product: {name}</span>
      <p>Description: {description}</p>
    </div>
  )
}

export default Card

Attention, la partie à l'intérieur du return n'est pas de l'HTML, mais du JSX... On va y revenir.

Hiérarchie de composants

Chaque component peut faire appel à d'autres components. On construit ainsi notre "arbre" de components.

import React from 'react'
import Card from './Card'

const Parent = () => {
  return (
    <div>
      <Card name="Game of thrones" description="Greats Books" />
      <Card name="The Lion King" description="Super movie" />
    </div>
  )
}

export default Parent

React et Virtual DOM

Le Document Object Model (ou DOM) fournit une représentation structurée du document sous forme d'un arbre et définit la façon dont la structure peut être manipulée.

Le DOM peut être modifié en javascript comme suit:

document.getElementsByTagName('p')[0].innerHTML = "Injected content in first <p>"

Cependant chaque accès au DOM est coûteux... La solution de React est de garder une représentation internet du DOM: le Virtual DOM.

React et Virtual DOM

React met à jour en permanence le Virtual DOM mais n'applique que les changements vraiment nécessaires au DOM (React Fiber Reconciliation).

Créer notre première App

Avec Vite

Pour créer notre application React, on va utiliser l'outil Vitejs au lieu de l'outil CRA:

npm create vite@latest my-react-app -- --template react-ts

Il est également possible d'utiliser create-react-app, mais cette option n'est aujourd'hui plus recommandée car moins performante.

Documentation: https://github.com/facebook/create-react-app

Point d'entrée

Le point d'entrée sera notre index.js. Ce dernier va injecter dans dans notre index.html dans la div ayant l'id root le contenu de notre App.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

TypeScript avec ESLint

Afin de d'avoir un meilleur expérience développeur, de detecter des erreurs des l'écriture du code, et de garder un même style de code au sein du projet, on va utiliser eslint avec typescript.

npm install --save-dev eslint
yarn add -D eslint

On devra ensuite initialiser eslint:

npx eslint --init
▸ To check syntax and find problems
▸ JavaScript modules (import/export)
▸ React
? Does your project use TypeScript? ‣ Yes
✔ Browser
✔ Node
▸ JSON
? Would you like to install them now with npm? ‣ Yes

TypeScript avec ESLint (2)

Il faudra ajouter les propriétés au fichier .eslintrc comme suit:

{
  "env": {
    "jest": true // À ajouter
  },
  "plugins": [
	"@typescript-eslint",  // À ajouter
	"prettier"  // À ajouter
  ],
  "extends": [
    "prettier" // À ajouter
  ]
}

On notera qu'on prépare l'installation de prettier.

ESLint avec Prettier

Enfin, on va pouvoir installer Prettier afin de garder le meme formatage du code au sein de notre projet:

npm install -D prettier eslint-config-prettier eslint-plugin-prettier

Puis créer un fichier de configuration pour Prettier:

touch .prettierrc

Et on ajoute dans notre package.json:

{
  "semi": false,
  "tabWidth": 2,
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all",
  "jsxSingleQuote": true,
  "bracketSpacing": true
}

Avec nos différentes règles:

{
  "scripts": {
    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx,json}'",
    "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx,json}'",
    "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc"
  }
}

Absolute Path

Plutot que d'importer nos composants depuis un chemin relatif, on peut utiliser des chemin absolus:

// On écrira alors: 
import Loader from '@/components/Loader';
//Plutôt que
import Loader from '../../components/Loader';

Pour ce faire il faudra dans le fichier tsconfig.json ajouter:

npm i -D vite-tsconfig-paths

Et ajouter le plugin dans notre vite.config.ts:

"paths": {
  "@components/*": ["./src/components/*"]
}

Installer la librairie:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
});

JSX

JSX

Le JSX, pour JavaScript & XML, est une extension à JavaScript proche du XML.

Le JSX produit des "éléments" ou composants React.

const name = 'Fabio'
const element = (<h1>Bonjour, {name}</h1>)

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(element);

Le JSX n'est pas obligatoire pour développer sous React (car il est ensuite compilé par React et devient une simple fonction JS). Il est cependant fortement recommandé car beaucoup plus lisible.
C'est un "syntaxic sugar".

JSX - createElement

Ces deux versions sont toutes deux équivalentes:

const Button = ({name}) => {
  return <button>Send to {name}</button>
}

root.render(
  <Button name="Fabio" />
)

Les balises JSX peuvent être soit des balises HTML, soit des components que l'on a créer. Les components que l'on créer commencent toujours par une majuscule.

const Button = ({name}) => {
  return React.createElement('button', {name}, `Send to ${name}`)
}

root.render(
  React.createElement(Button, {name: 'Fabio'}, null)
)

Règles: un seul nœud

On ne peut retourner qu'un seul nœud/qu'une seule racine.

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <h2>Profil</h2>
    <UserCard id={1} name="Fabio"></UserCard>
  )
}
// Ne va pas compiler car deux éléments sont retournés

Il faudra écrire:

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <div>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </div>
  )
}
// Pas de problème ici

JSX

Pour renvoyer les deux éléments, sans rajouter de parent englobant les deux, on peut utiliser le Fragment

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <React.Fragment>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </React.Fragment>
  )
}

Ou sa forme raccourcie:

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </>
  )
}

JSX

Les balises n'ayant pas de contenu/d'enfants peuvent être self-closing (auto-fermantes).

<div>
    <span></span>
    <UserCard id={1} name="Fabio"></UserCard>
</div>

// revient à écrire
<div>
    <span />
    <UserCard id={1} name="Fabio"/>
</div>

Cela diffère donc de l'HTML.

Et certains mot-clés sont réservés en JavaScript:

// HTML
<label class="bigLabel" for="someInput"/>

// JSX
<label className="bigLabel" htmlFor="someInput"/>

CSS

Pour mettre en place le CSS sur React, plusieurs solution s'offrent à nous:

On peut faire du css inline:

const image = 'someurl.com/img.jpg'
const divStyle = {
  color: 'blue',
  backgroundImage: `url(${image})`,
}

const HelloWorldComponent = () => (<div style={divStyle}>Hello World!</div>)

Dans la pratique, on préfère les fichiers CSS séparés:

/* myComponent.module.css */

.red {
    color: red;
}
import styles from './myComponent.module.css'

const myComponent = () => {
  return (
    <p className={styles.red}>It will be red</p>
  )
}

Props

Les props

Chaque composant que l'on crée accepte un et un seul argument: props. Cet argument sera toujours un objet.

Une props est une propriété que l'on passe du composant parent au composant enfant.

L'exemple suivant montre un composant utilisant la propriété name:

import React from 'react'

export const Welcome = (props) => {
  return (
    <p>
      Bonjour {props.name}
    </p>
  )
}

Pour utiliser une variable javascript en jsx, on utilise les accolades.

Props

On peut passer plusieurs propriétés à un composant. Il suffit pour ça de spécifier un nom d'attribut (à gauche) suivi de sa valeur (à droite) (comme on le ferait en HTML).

Pour accéder à ces propriétés, on peut le faire:

- via this.props dans les class component

- via l'argument props pour les functional component

<MyComponent propsA={valueA} propsB={valueC}/>

<UseCard name="Tony Start" hero="Iron Man"/>

⚠️ Il ne faut jamais muter les valeurs passées par les props dans un composant.

Passer des props

ReactDOM.render(
  <Welcome name="Fabio" />,
  document.getElementById('root')
)

Mais on peut aussi passer une expression Javascript.

// Le résultat d'une fonction
<Welcome name={"FABIO".toLowerCase()}/>
<Welcome id={33 + (25 * 100)}/>

// Une variable, quel que soit le type (string, array, object, etc)
const myName = "Fabio"
<Welcome name={myName}/>

// La référence d'une fonction
<button onClick={() => { console.log("clicked !"); }}/>

On peut passer tous les types dans une props, comme ici une chaîne de caractères (string).

Props et Spread Operator

On peut également utiliser le spread operator lorsque l'on a plusieurs props à passer à un composant:

const user = {
  name: "Fabio",
  gender: "male"
}

return (
  <Welcome {...user} />
)

Equivalent à:

const user = {
  name: "Fabio",
  gender: "male"
}

return (
  <Welcome name={user.name} gender={user.gender} />
)

Props et valeurs par défaut

Si l'on passe une props sans valeur, celle-ci prendra la valeur true par défaut:

<SomeComponent visible />
// est équivalent à
<SomeComponent visible={true} />

Props.children

children est une propriété de l'objet props qui est toujours passée dans ce dernier. Elle contient les (éventuels) enfants du composant:

const DisplayThreeTimesComponent = (props) => {
    return <div>
        {props.children}
        {props.children}
        {props.children}
    </div>
}


const ParentComponent = () => {
    return (
      <div>
          <Header />
          <DisplayThreeTimesComponent>
              <p>Hello !</p>
          </DisplayThreeTimesComponent>
      </div>
    )
}

Exercice: Passer des props

Cloner le répertoire suivant:

https://github.com/FabioReact/exercice-react

git clone https://github.com/FabioReact/exercice-react.git
cd exercice-react
npm install / npm i / yarn
npm run dev / yarn dev

Boucler

Voici comment boucler sur un itérable et de le retourner en JSX:

const names = ['Emmanuel', 'Martin', 'Dupont']
const arrayOfLi = names.map((name) => <li>{name}</li>)
return (
  <ul>
    {arrayOfLi}
  </ul>
)
// Ou encore
const names = ['Emmanuel', 'Martin', 'Dupont']
return (
  <ul>
    {names.map(name => <li>{name}</li>)}
  </ul>
)

Exercice sur les boucles

keys

Quand on crée des éléments dans une liste, typiquement en bouclant, React a besoin d'un identifiant unique

(unique pour cette liste).

const names = ['Tony', 'Steve', 'Natasha']
return (
  <ul>
    {names.map((name, index) => <li key={index}>{name}</li>)}
  </ul>
)

React utilise ces identifiants pour savoir quel élément a disparu, apparu, ou a été changé.

Documentation: https://fr.reactjs.org/docs/lists-and-keys.html

Attention, dans cet exemple on utilise l'index du tableau comme identifiant unique... C'est une mauvaise pratique! Il faudrait utiliser un id vraiment unique (comme ceux de la base de données).

propTypes - sans TypeScript

import PropTypes from 'prop-types'

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    )
  }
}

Greeting.propTypes = {
  name: React.PropTypes.string
}

Indique le type attendu de chacune des props

Est vérifié au runtime

Alternative avec TypeScript

type Props = {
  name: string;
};

const Greeting = (props: Props): JSX.Element => {
  return (
    <h1>Hello, {props.name}</h1>
  );
};

Indique le type attendu de chacune des props.

Si le type n'est pas correct, on aura des erreurs dans la console ainsi que dans notre IDE.

defaultProps

class Welcome extends React.Component {
  render() {
    return (
      <h1>Welcome, {this.props.name}</h1>
    )
  }
}

Welcome.defaultProps = {
  name: "Fabio"
}

Valeur par défaut des props, pour la syntaxe avec class

Pour les functional component, il suffit simplement d'utiliser les features d'ES6!

const Greeting = ({name = "Emmanuel"}) => {
    return <h1>Hello, {name}</h1>;
}

Structure de notre application

Export

Afin qu'un composant soit utilisable ailleurs que dans le fichier où il a été créé, il faudra l'exporter.

Cependant, il est possible exporter une fonction / un composant de plusieurs manières:

Export par défaut:

const MyComponent = () => {
  // ...
}

export default MyComponent

Export nommé (avec et sans alias):

const MyComponent = () => {}
const MySecondComponent = () => {}

export { MyComponent, MySecondComponent as MSComponent }

Il n'est possible de faire qu'un seul export par défaut par fichier, mais on peut faire autant d'export nommé que l'on souhaite:

export { fonction1 as default, fonction2, fonction3 }

Import

On devra ensuite importer la fonction / le composant conformément à la méthode utilisée pour l'exporter:

Import par défaut:

import MyComponent from "./path/to/component";

Import nommé:

import { MyComponent } from "./path/to/component";

Il est aussi possible de faire un import par défaut suivi d'imports nommés:

import fonctionParDefaut, { fonction2, fonction3 } from "./path/to/functions";

Il est utile de noter qu'on n'est pas obligé de conserver le nom d'origine grâce à l'import par défaut

import MonComposantRenomme from "./path/to/component";

Je suis obligé d'importer le composant par son nom. Si je souhaite le renommer, je pourrais le faire via un alias.

import { MyComponent as MonComposantRenomme } from "./path/to/component";

Structure de fichiers

React n'a pas d'opinion sur la structure de fichiers à adopter. Cette structure peut donc varier d'un.e projet/entreprise/équipe à un.e autre.

On peut cependant organiser son projet par fonctionnalité ou par type de fichiers.

src 
│
└───hooks
│   │   useCustomHook.jsx
│   
└───components
│   │   Navbar.jsx
│   │   Header.jsx
│   │   Footer.jsx│  
│
└───pages
│   │   Home.jsx
│   │   Blog.jsx

Data Flow - Données et évenements

Data Flow

Les props ne vont que dans un seul sens : des composants parents vers les enfants.


Par conséquent, les composant enfants ne peuvent pas directement influencer l'affichage des parents.

 

Il existe cependant un moyen (indirect) de le faire:

les parents peuvent passer en props une référence à une fonction au composant enfant, fonction qui sera capable de modifier l'état du parent.

Data Flow

Data Flow

La plupart des Components de l'application vont être très simple.

 

On va avoir un ou quelques "container" components, à la racine de l'application, qui vont devoir se connecter d'une manière ou d'une autre au reste de l'application....

 

Dépend de la solution choisie : fait maison? Redux? React Context?

Class Component Lifecycle
(Déprécié)

Introduction

On peut diviser le cycle de vie d'une composant en 3 étapes successives:

1. Création (Mount)

2. Mise à jour (Update)

3. Destruction (Unmount)

Création (Mount)

Phase d'initialisation du composant.

Le composant ainsi que tous ses enfants sont montés dans l'UI.

A ce stade, ni les props, ni le state ne sont encore définis.

Création (Mount)

class MyComponent extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  static getDerivedStateFromProps(nextProps, prevState)
  {
    if (true) {
      return someNewState
    }
    return null
  }

  render() {
    // JSX
  }
 
  componentDidMount() {
    // Code After-render
  }
  
}

initialisation du state

rafraîchit l'UI

Le composant est désormais monté, on peut maintenant faire des requêtes à la bdd

met à jour le state en fonction des props

Mise à jour (Update)

class MyComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Renvoie "true" par défaut
  }

  
  
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
	// Utilise si on a besoin de faire
	// des calculs selon l'état précédent
  }
 

  componentDidUpdate() {
    // Après le re-render
  }

  
  
  render() {
    // re-render
  }
  
}

Retourne un booléen, permet d'éviter le rafraîchissement inutile du composant

appelée après que le nouveau render ai eu lieu

rafraîchit l'UI

permet de capturer des infos du DOM courant

Destruction (Unmount)

class MyComponent extends Component {
  componentWillUnmount() {
	// Code unsubscribe
  }
}

Le composant n'est plus dans l'UI, endroit idéal pour se désabonner à d’éventuels événements.

Si vous ne le faites pas, vous vous exposez à des fuites mémoires.

Attention!

React Hooks

useState

import React, { useState } from 'react';

const Count = (props) => {
    const initialCount = 0; // Peut être un array [], un object {},
    // une string "chaine", un int...
    const [count, setCount] = useState(initialCount);
    return (<>
      Total : {count}
      <button onClick={() => setCount(initialCount)}>Réinitialiser</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>);
}
// Ici, utilisation de React.Fragment dans sa forme réduite: <> et </>
export default Count;

useState renvoie un tableau dont la première valeur est le state (getter), et la seconde une fonction (setter) permettant de modifier le state.

Depuis React v16.8, les functional components peuvent aussi avoir un state:

Attention, useState ne merge pas la nouvelle valeur avec les anciennes, utiliser le spread operator!

useEffect

useEffect est une alternative pour les functional component au lifecycle des class component. Il s'exécute à chaque render.

Il prend deux paramètres:

  1. Le premier est la fonction à exécuter lorsque celui-ci est déclenché
  2. Le second un array de paramètre(s) déclencheur(s)
import React, { useEffect } from 'react';

const SomeComponent = (props) => {
   	// Some code
    useEffect(() => {
      // Effect
	  return () => {
         // Clean up effect
	  };
    }, [input]);
   	// Some Code
    return (
      //Some JSX
    )
}

useLayoutEffect

useLayoutEffect a la même signature que useEffect. La différence entre ces derniers et le moment où les effets sont exécutés.

useLayoutEffect s'exécute de manière synchrone après une mise à jour et donc avant que le navigateur ne repeigne la page.

Lorem

import React, { useLayoutEffect } from 'react';

const SomeComponent = (props) => {
   	// Some code
    useLayoutEffect(() => {
      // LayoutEffect
	  return () => {
         // Clean up effect
	  };
    }, [input]);
   	// Some Code
    return (
      //Some JSX
    )
}

useRef

useRef est l'alternative de createRef pour la création d'une référence dans un functional component.

L'objet retourné par useRef persistera pendant toute la durée de vie du composant. Les références sont utiles notamment pour accéder facilement au DOM.

import React, { useRef } from 'react';

const SomeComponent = (props) => {
  const usernameRef = useRef(null);
  const login = () => {
    const username = usernameRef.current.value;
    // Some code...
  }
  return (
    <>
      <input ref={usernameRef} type="text" id="username" />
      <button onClick={login}>Submit</button>
    </>
  )
};

useReducer

useReducer est une alternative de useState. Il est parfois plus aisé d'avoir un seul reducer plutôt qu'une multitude de useState.

import React, { useReducer } from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {counter: state.counter + 1};
    case 'decrement':
      return {counter: state.counter - 1};
    default:
      throw new Error("Incorrect action type");
  }
}

const Counter = (props) => {
  const initialState = {counter: 0};
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <p>Compteur: {state.counter}</p>
      <button onClick={() => dispatch({type: 'increment'})}>Increment</button>
      <button onClick={() => dispatch({type: 'decrement'})}>Decrement</button>
    </>
  )
};

useTransition

Depuis la version 18 de React, il est possible d'utiliser le concurrent mode afin de prioriser certaines mise à jour de l'UI.

useTransition permet de spécifier des mises à jour de l'UI comme non urgente et qui puissent être interrompues. Cela est utile lorsque on a un affichage lent et qu'on puisse éventuellement différer (ex: un filtre).

import React, { useTransition } from 'react';

const MyComponent = (props) => {
  const [isPending, startTransition] = useTransition()
  
  const someEventHandler = (event) => {
    startTransition(() => {
      // Toutes les mises à jour faite ici sont déclarées comme non urgente et peuvent être interrompues
      setValue(event.target.value);
    });
  }

  return (...)
};

useDeferredValue

useDeferredValue est similaire à useTransition mais à la différence qu'il s'utilise dans un composant enfant et dont les information proviennent du parent. Si une mise à jour plus urgente est faite, React retournera la valeur précédente et ne mettra à jour que par la suite les mises à jour non urgentes.

import React, { useDeferredValue } from 'react';

const MyComponent = ({ list }) => {
  const deferredValue = useDeferredValue(list);

  return (
    <>{deferredValue.map(el => <OtherComponent info={el} />)}</>
  )
};

Pour tirer tout les avantages de useDeferredValue il faudra également penser à mémoïser notre composant.

Conditional Rendering

Affichage Conditionnel

function HelloUser(props) {
  if (!props.name) return (<p>Hello visitor</p>)
  return (<p>Hello {props.name}</p>)
}

Il est possible d'afficher un composant ou non en fonction d'une condition donnée.

La première solution est d'utiliser une condition if:

Ou une condition ternaire:

function HelloUser(props) {
  return (<p>Hello {props.name ? props.name: 'visitor'}</p>)
}

Ou stocker le composant dans une variable:

function HelloUser(props) {
  let component = null
  if (props.name) component = (<p>Hello {props.name}</p>)
  else component = (<p>Hello visitor</p>)

  return component
}

&& - ||

function HelloUser(props) {
  return (
    <p>Hello gamer</p>
    {props.points && <p>You have {props.points} points so far. Good job!</p>}
  )
}

L'opérateur logique && permet de renvoyer ce qui sera à droite de ce dernier si ce qui est à gauche est truthy:

L'opérateur logique || permet de renvoyer le premier élément qui sera truthy:

function HelloUser(props) {
  return (
    <p>Hello gamer</p>
    <p>You have {props.points || 0} points so far.</p>
  )
}

Formulaires

Forms

<input
  type="text"
  value="My value"
/>

Dans un formulaire, on peut spécifier la value d'un input. On appelle alors cela un Controlled input.

Pour ce faire, on spécifie la propriété value.

Mais du coup, comment celle-ci peut changer?

Forms (Controlled)

  constructor(props) {
    super(props);
    this.state = {inputValue: "My default value"};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleChange(event) {
    this.setState({inputValue: event.target.value});
  }
  handleSubmit(event) {
    console.log('The submitted value was ' + this.state.inputValue);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type="text"
          value={this.state.inputValue}
          onChange={this.handleChange}
        />
        <button>OK</button>
      </form>
    );
  }

On doit donc écouter manuellement les changements, les appliquer au state, et utiliser le state pour la value.

Forms (Controlled) - Hooks

  const [inputValue, setInputValue] = useState('')
  
  const handleChange = (event) =>  {
    setInputValue(event.target.value)
  }
  
  const handleSubmit = (event) => {
    event.preventDefault()
    console.log('The submitted value was ' + inputValue)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
      />
      <button>OK</button>
    </form>
  );

On doit donc écouter manuellement les changements, les appliquer au state, et utiliser le state pour la value.

Forms (Uncontrolled)

constructor(props) {
  super(props);
  this.inputRef = React.createRef();
}

handleSubmit(event) {
  console.log('we got the value ' + this.inputRef.current.value);
  event.preventDefault();
}

render() {
  return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <input 
          type="text"
          defaultValue="A default value"
          ref={this.inputRef} />
        <button>OK</button>
      </form>
  );
}

On ne spécifie pas la prop value (on peut juste spécifier defaultValue), et on utilise ref pour récupérer la valeur.

Forms: Quelle option choisir?

Plusieurs librairies existent pour nous aider à gérer les formulaires (React Hook Form étant la plus utilisée).

Optimisations

React Lazy

Permet d'afficher du JSX ou un autre component en attendant le chargement du 'lazy' component.

import React, { Suspense, lazy } from 'react';
import Profile from './components/Profile';

const User = lazy(() => import('./components/User'));

const App = () => (
  <React.Fragment>
    <Profile />
    <Suspense fallback={<div>Loading...</div>}>
      <User />
    </Suspense>
  </React.Fragment>
);

Pure Component - Classes

Pure Components est une alternative à l'utilisation de la méthode shouldComponentUpdate des class components.

Ce dernier effectue un shallow check du state et des props. S'il n'y a eu aucun changement, le composant ne sera pas mis à jour.

import React, { PureComponent} from 'react';

class SomeComponent extends PureComponent {
    // Some Code
    render() {
      return (
      	// Some JSX
      );
    };
};

Très efficace pour éviter des rendu inutiles.
N'utiliser que PureComponent si les props ou le state du parent peuvent vraiment provoquer un render inutile du component enfant.

React Memo

React Memo est une alternative à l'utilisation de la méthode shouldComponentUpdate pour les functional components.

Même objectif que les PureComponent.
Attention, n'effectue qu'une shallow comparaison et peut ne pas détecter des changements à l'intérieur d'un tableau, objet (reference types).

import React from 'react';

const SomeComponent = (props) => {
    // Some Code
    return (
      // Some JSX
    );
};

export default React.memo(SomeComponent);

useCallback

useCallback permet de renvoyer une fonction de rappel mémoïsée. Pour cela on doit passer la fonction que l'on souhaite mémoïser, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:

import React, { useCallback } from 'react';

const SomeComponent = (props) => {
  // ...
  const memoizedCallback = useCallback(
    () => {
      doSomething(a, b);
    },
    [a, b],
  );
  // ...
};

Cela permet d'éviter des rendu inutiles lorsqu'on passe une fonction à un composant utilisant shouldComponentUpdate, React.memo, ou PureComponent.

useMemo

useMemo permet de renvoyer une valeur mémoïsée. Pour cela on doit passer la fonction dont on souhaite obtenir le résultat, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:

import React, { useMemo } from 'react';

const SomeComponent = (props) => {
  // ...
  const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  // ...
};

Si les inputs n'ont pas changé, alors on gardera la valeur précédente du retour de la fonction. 

Cela est très utile lorsqu'on fait appel à une fonction qui fait des calcul coûteux.

Si on ne fournis aucun tableau, une nouvelle valeur sera calculée à chaque appel.

React Context

React Context

Le contexte sert à partager une valeur entre différents composants quel que soit la distance entre ces derniers.

La première étape est de créer un contexte.

// theme-context.js
import{ createContext } from 'react';

const ThemeContext = createContext({
    foreground: "#000000",
    background: "#eeeeee"
});

export default ThemeContext;

Les valeurs renseignées dans la méthode createContext ne sont pas des valeurs pas défaut. Celles-ci seront néanmoins utiles pour de l'autocompletion lors de l'utilisation du context.

Vous pouvez créer autant de contexte différent que vous le souhaitez.

React Context - Provider

La deuxième étape sera de preciser quelle partie de l'application aura accès a ce contexte. Seuls les enfants de la balise Provider aura accès ensuite au contexte donné.

// App.jsx
const App = () => {
  return (
    <>
      <ThemeContext.Provider value={{
        foreground: "#ffffff",
        background: "#222222",
      }}>
        <Home />
      </ThemeContext.Provider>
      <Footer />
    </>
  );
}

Dans cet exemple, le footer n'aura pas accès au contexte du thème.

React Context - useContext

La derniere étape sera de consommer notre contexte. Il existe deux façon de le faire. Cette dernière reste la plus simple:

// ThemeButton.jsx
import ThemeContext from "./context/theme-context";

const ThemedButton = () => {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      Je suis stylé par le contexte de thème !
    </button>
  );
}

export default ThemeContext;

Ou encore de créer un custom hook:

import ThemeContext from "./context/theme-context";

const useTheme = () =>  useContext(ThemeContext);

export default useTheme;

React Context - Consumer

On peut aussi consommer le contexte via la balise Consumer comme suit:

// ThemeButton.jsx
import ThemeContext from "./context/theme-context";

const ThemedButton = () => {
  return (
    <ThemeContext.Consumer>
      {(theme) => (
        <button style={{
          background: theme.background,
          color: theme.foreground
        }}>
          Je suis stylé par le contexte de thème !
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeContext;

Unit Test

Installation de vitest

Afin de faire mettre en place des test unitaires sur un projet React mis en place avec Vite, on va devoir installer Vitest, 

npm install -D vitest

Une fois cette dépendance ajoutée, on peut ajouter à nos scripts:

{
  ...
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest"
  },
  ...
}

Configuration

On ajoute ensuite ceci au fichier vite.config.ts afin de créer notre configuration de test:

/// <reference types="vitest" />
/// <reference types="vite/client" />

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./setupTests.ts'],
  },
});

Premier ficher de test

On peut maintenant écrire notre premier test.

describe permet de créer un bloc qui regroupe plusieurs tests liés.

it, ou test décrit un test. Le premier argument décrit ce qu'on cherche à tester, et le second est la fonction qui contient nos assertions.

import { describe, it, test, expect } from 'vitest';

describe('something truthy and falsy', () => {
  it('should be true', () => {
    expect(true).toBe(true);
  });

  test('false to be false', () => {
    expect(false).toBe(false);
  });
});

Installation de testing-library

On va enfin installer testing library qui nous permet de tester une application React

npm install --save-dev jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @types/jest
import '@testing-library/jest-dom'

On créer enfin le fichier setupTests.ts qui va s'exécuter afin la suite de tests. Cela permet de rendre disponible certaines méthodes dans nos tests:

Création d'un fichier de test

On peut maintenant utiliser les fonction de testing libraby pour tester notre composant:

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Fetch from './fetch'

test('loads and displays greeting', async () => {
  // render permet de charger notre composant dans notre test
  render(<Fetch url="/greeting" />)

  // userEvent permet de simuler des actions utilisateur
  await userEvent.click(screen.getByText('Load Greeting'))
  // getBy throw une erreur si le noeud n'est pas trouvé contrairement à findBy
  await screen.findByRole('heading')

  // ASSERT
  expect(screen.getByRole('heading')).toHaveTextContent('hello there')
  expect(screen.getByRole('button')).toBeDisabled()
})

Redux - Concepts

Principes

Les 3 principes de redux sont les suivants:

Single source of truth

L'état de toute notre application est stocké dans un seul objet store.

State is read-only

Le seul moyen de changer le state est via l'émission (dispatch) d'une action. Une action décrit ce qu'il vient de se passer.

Changes are made with pure functions

L'état de notre application (store) sera changé à partir de fonctions pures: les reducers.

Une fonction pure retourne toujours le même output pour un input donné, et ne doit pas avoir d'effets de bord.

Fonctionnement

Le store contient l'état de notre application. Pour changer cette état, on va dispatch des actions. Ces actions vont être traités par un reducer qui va déterminer l'état futur de notre application en fonction de l'action qui lui a été transmise.

View

Action

Dispatch

Reducer

Store

Fonctionnement

Store

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

Le store (état de notre application) est un simple objet javascript:

Actions

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

Les actions sont également des objets javascript. Cependant celles-ci doivent impérativement contenir la propriété type (ce dernier étant une string).

D'autres propriétés peuvent lui être ajoutés, elles sont optionnelles dans l'absolu mais peuvent être nécessaire selon le traitement que vous en faîte dans le reducer.

Actions Creators

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}

Les actions creators sont des fonctions qui retournent une action.

Les actions creators vont notamment nous être utile pour la mise en place de requêtes asynchrones.

Reducer

const initialState = {};

function rootReducer(state = initialState, action) {
  // On mettra en place ici un switch/case pour une faire un traitement particulier
  // selon le type de l'action qui aura été passé en argument lors du dispatch
  // et ainsi retourner le nouvel état de l'application
  return state
};

Un reducer est une fonctions qui a la spécificité de devoir être pure.

Le reducer doit prendre comme premier argument le state courant (qu'il faudra initialiser au départ) et pour second l'action qui lui sera dispatch.

Redux avec React (legacy)

Packages

npm install --save redux react-redux
OU
yarn add redux react-redux

Pour installer redux dans notre application react, il nous faudra installer les deux packages suivant:

redux et react-redux

Le Provider

// src/index.js
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './store/reducer';

const myStore = createStore(rootReducer);

ReactDOM.render(
  <Provider store={myStore}>
    <App />
  </Provider>,
  document.getElementById('root')
);

On souhaite que toute notre application react ai accès à redux, pour cela on va mettre notre composant Provider (fournit par react-redux) à la racine de notre application:

Pour créer notre store, on fait appel à la fonction createStore de redux. Pour ce faire on doit passer en paramètre à createStore notre reducer.

Le Reducer

// src/store/reducer.js
import * as actionTypes from './actionTypes'

const initialState = {
  todos: []
};

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      }
    default:
      return state
  }
}

Ici, on importe les action types d'un autre fichier js. Il faut alors le créer.

Les Action types

// src/store/actionTypes.js
export const ADD_TODO = "ADD_TODO"

Par convention, les types des actions sont en uppercase. Le fait de stocker nos types d'actions dans une variable permet de générer des erreurs lors de la compilation si on fait une faute de frappe (lors de la création d'une action ou lors de sa lecture dans le reducer).

A partir de cette étape, on a créer avec succès notre store. On va pouvoir désormais connecter nos composants avec le store.

// src/store/actionCreators.js
import * as actionTypes from './actionTypes'
export const addToDo = (text) => {
  return {
	type: actionTypes.ADD_TODO,
	text: text
  }
}

Connecter nos composants

On va maintenant voir comment "connecter" un de nos composant avec redux. Une fois un composant connecté, il recevra via ses props ce que l'on aura déterminé.

On recevra deux types de props:

  • des données provenant du store, que l'on va pouvoir récupérer via la fonction mapStateToProps (va injecter dans nos props le state que l'on aura précisé de récupérer)
  • des fonctions callback, pour la mise à jour du store, que l'on pourra récupérer via mapDispatchToProps

Connecter nos composants

// src/pages/ToDoComponent.js
import React from "react"
import { connect } from "react-redux"
import { addToDo } from "../store/actionCreators.js"

const ToDoComponent = props => {
  // ...
}
// mapStateToProps reçoit comme paramètre le store actuel de redux
const mapStateToProps = state => {
  return {
    todos: state.todos // props.todos (type []) sera accessible dans notre component
  }
}
// mapStateToProps reçoit comme paramètre la fonction dispatch de redux
const mapDispatchToProps = dispatch => {
  return {
    onAddToDo: (text) => dispatch(addToDo(text)) // props.onaddTodo (type fn) sera  aussi accessible
  }
}
// connect permet de connecter ce que l'on vient de définir à notre composant
export default connect(mapStateToProps, mapDispatchToProps)(ToDoComponent)

Afin de connecter notre composant, il faut procéder comme suit:

Redux DevTools

import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import someMiddleware from './someOMiddleware';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
    reducer,
    composeEnhancers(applyMiddleware(someMiddleware))
)

Redux DevTools est une extension que l'on peut installer sur son navigateur afin d'avoir des outils de développement/debug.

  1. Installer l'extension sur votre navigateur (https://github.com/zalmoxisus/redux-devtools-extension)
  2. Modifier l'index.js

Redux asynchrone (legacy)

Asynchrone et redux-thunk

npm install --save redux-thunk
OU
yarn add redux-thunk

Jusqu'ici, tout est synchrone. Les dispatch font appel au reducer et ce dernier doit être pure...
Comment faire donc de l'asynchrone afin de faire un appel à la base de données?

Dès que l'on travaille avec un API, on enfreint les règles d'une fonction pure car:

  • on a un effet de bord
  • notre réponse est forcément asynchrone

Pour résoudre notre problème, on va utiliser le middleware: redux-thunk

redux-thunk

redux-thunk est une middleware qui va se positionner entre le dispatch de l'action et notre reducer, ce qui lui permet d'effectuer un traitement particulier. On va faire ça en dispatchant un nouvel action creator qui pourra être asynchrone grâce à redux-thunk.

Store

Reducer

Dispatch

Async Action

View

API call

Dispatch

Action

Mise en place

// src/index.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk'
import { Provider } from 'react-redux';
import rootReducer from './store/reducer';

const withMiddleware = applyMiddleware(thunkMiddleware)
const myStore = createStore(rootReducer, undefined, compose(withMiddleware));
ReactDOM.render(
  <Provider store={myStore}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Une fois notre package redux-thunk installé, on va le mettre en place sur notre projet:

On fait appel à notre thunkMiddleware, et on utilise la fonction applyMiddleware de redux afin de l'appliquer, applyMiddleware peut prendre autant de middleware en arguments que besoin.

compose de redux est une fonction afin de composer avec nos (possibles) différents enhancers (withMiddleware est un enhancer).

Async Action types

// src/store/actionTypes.js
export const ADD_TODO = "ADD_TODO"

On peut maintenant créer des action creators asynchrones. Ces derniers pourront par la suite appeler des action creators à travers la fonction dispatch.

// src/store/actionCreators.js
import * as actionTypes from './actionTypes'
export const addToDo = (text) => {
  return {
	type: actionTypes.ADD_TODO,
	text: text
  }
}

export const asyncAddToDo = (text) => {
  return dispatch => {
    // Exécuter de l'asynchrone ici...
    await response = fetch("URL")
    dispatch(addToDo(text))
  }
}

Dans notre composant...

// src/pages/ToDoComponent.js
import { asyncAddToDo } from "../store/actionCreators.js"

const mapDispatchToProps = dispatch => {
  return {
    onAddToDo: (text) => dispatch(asyncAddToDo(text))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ToDoComponent)

Le composant devra désormais uniquement appelé notre action creator asynchrone.

Class Components
(Déprécié)

Première Syntaxe: Class

class App extends React.Component

Ou encore

import React from 'react'

class MyComponent extends React.Component {
  constructor(props) {
      super(props);
      // ...
  }

  someCustomMethod(){
    // Code
  }    

  render() {
    return (<div>
      // Some Content
    </div>)
  }
}
import React, { Component } from 'react'

class MyComponent extends Component {
  // ...
}

Seconde Syntaxe: 

Les "functional components", la syntaxe est plus succincte, mais son comportement est cependant différent (cycle de vie). Se résume à écrire la méthode render() en prenant les props comme argument.

const MyComponent = (props) => {
    return (
      <div>
        { (...) }
      </div>
    );
}

Quelle syntaxe choisir?

Class Component vs Functional Component

Utilisez une classe lorsque vous avez besoin d'un state / lifecycle hooks et que vous ne pouvez pas utiliser les React Hooks (v16.8), sinon, utilisez un Functional component.

Class Component

class Learn extends Component

  • Possède un state
  • Accès au cycle de vie

Functional Component

const Learn = () => {...}

  • Possède un state depuis v16.8
  • Cycle de vie depuis v16.8

Utilise "this" pour accèder au state/props:

    this.state.someVariable

    this.props.someVariable

Accède directement aux props:

    someState

    props.someVariable

State

Le state est un objet qui représente un état interne du composant.

 

Lorsque une valeur dans le state est "mise à jour", alors l'application de met à jour.

 

Un usage serait un composant dont l'apparence changerait en fonction d’événements internes, et qui n'impacte pas le fonctionnement des composants parents.

Exemple: un carrousel

this.state

Il est uniquement possible d'y accèder de cette manière dans la syntaxe de classe.

this.state.someProperty pour lire les valeurs

this.setState({ someProperty : "something "}) pour setter les valeurs

this.setState() merge intelligemment (mais shallow) avec le valeurs courantes, pas besoin de tout redéfinir.

On ne doit jamais setter de cette de cette façon:

this.state = (...); 

...sauf dans le constructeur (phase d'initialisation du state) !

this.state

class TogglableImage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {displayImage: true};
  }

  toggleImage() {
    this.setState({
      displayImage : !this.state.displayImage
    });
  }

  render() {
    return (
      <div>
        <button onClick={this.toggleImage.bind(this)}>Show/hide the image</button>
        {this.state.displayImage && <img src={logo}/>}
      </div>
    );
  }
}

Attention, dans cet exemple displayImage prend la valeur inverse de this.state.displayImage. Cette façon de procéder n'est pas correcte, on vas voir pourquoi...

this.setState()

// Peut se tromper
this.setState({
  counter: this.state.counter + 1
})


// Syntaxe toujours correcte !
// Reçoit le state courant et les props courantes au moment
// où l'update sera appliqué
this.setState((prevState, props) => ({
  counter: prevState.counter + 1
}))

Les setState() étant asynchrones, on n'a aucune certitude que la valeur précédente est bien celle à laquelle on s'attend.

Attention donc si vous dépendez des props courantes ou du state courant.

Attacher des events handler

class SayHello extends React.Component {

  handleClick() {
    // KO : "this" ici n'est pas la classe !
    this.setState({})
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Problème : perte du this

Attacher des events handler

class SayHello extends React.Component {

  handleClick() {
    // Ok
    this.setState({})
  }

  render() {
    // .bind(this) force à transmettre le this
    return (
      <button onClick={this.handleClick.bind(this)}>
        Say hello
      </button>
    );
  }
}

Solution 1 : .bind(this) à chaque usage

Non optimale car une fonction supplémentaire est appelée à chaque clique.

Attacher des events handler

class SayHello extends React.Component {

  constructor(props){
    super(props);
    // on bind à this et on écrase
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // OK
    this.setState(...)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Solution 2 : .bind(this) dans le constructor (la plus courante)

Attacher des events handler

class SayHello extends React.Component {

  constructor(props){
    super(props);
  }

  handleClick = () => {
    // OK
    this.setState({});
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Solution 3 : utiliser une arrow function (pas de changement de contexte avec cette dernière)

refs

constructor(props) {
    super(props);
    this.myRef = React.createRef();
}

componentDidMount(){
    // création d'une chart avec ChartJS
    new Chart(this.myRef.current, {
        (...)
    });
}


render() {
    // On utilise le `ref` callback pour stocker la référence vers
    // le noeud DOM dans un champ custom du this
    return (
      <div ref={this.myRef} />
    );
}

Une ref nous permet d'obtenir une référence vers un noeud du DOM du composant, afin d'en faire un usage spécifique.

React v18

By AdapTeach

React v18

Slides de formation

  • 704