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.
Documentation: https://github.com/facebook/create-react-app
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:
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
Beaucoup de types possibles:
https://fr.reactjs.org/docs/typechecking-with-proptypes.html
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:
- Le premier est la fonction à exécuter lorsque celui-ci est déclenché
- 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:
- Le premier est la fonction à exécuter lorsque celui-ci est déclenché
- 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.
- Installer l'extension sur votre navigateur (https://github.com/zalmoxisus/redux-devtools-extension)
- 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