React

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

Formateur: Fabio Ginja

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

Créer notre première App

create-react-app

Pour créer notre application React, on va utiliser l'outil fournit par facebook: create-react-app (CRA)

npx create-react-app my-first-app
cd my-first-app/
npm start

On a ainsi accès à une configuration avec Webpack et babel et cela en quelques secondes. C'est l'idéal pour démarer. Si on veut configurer webpack soi-même, on peut toujours faire un "eject", mais ce n'est pas réversible.

npm eject

Il est possible d'utiliser d'autres outils pour faire des modifications sans éjection (ex: craco).

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'
import App from './App'

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

JSX

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

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

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

ReactDOM.render(
  element,
  document.getElementById('root')
)

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

Ces deux versions sont toutes deux équivalentes:

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

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

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}`)
}

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

JSX

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 pourrq le faire:

- via this.props dans les class component

- via l'argument props pour les functional component

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

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

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

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"
}

ReactDOM.render(
  <Welcome {...user} />,
  document.getElementById('root')
)

Equivalent à:

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

ReactDOM.render(
  <Welcome name={user.name} gender={user.gender} />,
  document.getElementById('root')
)

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

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

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

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.

Components

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 mutable 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)

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 (indirecte) 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?

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.

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>
  )
}

Component Lifecycle

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: 'decrement'})}>Increment</button>
      <button onClick={() => dispatch({type: 'increment'})}>Decrement</button>
    </>
  )
};

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.

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, { 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

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.

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

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

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.

Redux ToolKit

React Testing Library

Jest

describe('Test suite', () => {
  test('Test case', () => {
    expect(true).toBe(true) // assertion
  })

  test('Test case', () => {
    
  })
})

Jest est un test runner qui contient également des méthodes afin de réaliser des tests double tels que des spies, stubs, ou mocks

npm run test
# Ou encore
yarn test

Dummy

describe('Test suite', () => {
  test('Test case', () => {
    expect(true).toBe(true) // assertion
  })

  test('Test case', () => {
    
  })
})

Jest est un test runner qui contient également des méthodes afin de réaliser des tests double tels que des spies, stubs, ou mocks

npm run test
# Ou encore
yarn test

Stub

describe('Test suite', () => {
  test('Test case', () => {
    expect(true).toBe(true) // assertion
  })

  test('Test case', () => {
    
  })
})

Un stub est un spy auquel on attache un comportement preprogrammer

npm run test
# Ou encore
yarn test

Mock

describe('Test suite', () => {
  test('Test case', () => {
    expect(true).toBe(true) // assertion
  })

  test('Test case', () => {
    
  })
})

Le mock sert a remplacer une dependance externe.

npm run test
# Ou encore
yarn test

React - Janvier 2022

By Fabio Ginja

React - Janvier 2022

Slides de formation

  • 878