Une bibliothèque JavaScript pour créer des interfaces utilisateurs
Formateur: Fabio Ginja
@Ambient-IT
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.
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.
Les trois frameworks les plus populaires restent Angular, React et Vue.js.
"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."
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.
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
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 met à jour en permanence le Virtual DOM mais n'applique que les changements vraiment nécessaires au DOM (React Fiber Reconciliation).
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
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>
);
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
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.
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"
}
}
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()],
});
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".
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)
)
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
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>
</>
)
}
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"/>
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>
)
}
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.
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.
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).
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} />
)
Si l'on passe une props sans valeur, celle-ci prendra la valeur true par défaut:
<SomeComponent visible />
// est équivalent à
<SomeComponent visible={true} />
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>
)
}
Cloner le répertoire suivant:
git clone https://github.com/FabioReact/exercice-react.git
cd exercice-react
npm install / npm i / yarn
npm run dev / yarn dev
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
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).
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
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.
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>;
}
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 }
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";
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
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.
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?
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)
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.
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
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
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!
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 est une alternative pour les functional component au lifecycle des class component. Il s'exécute à chaque render.
Il prend deux paramètres:
import React, { useEffect } from 'react';
const SomeComponent = (props) => {
// Some code
useEffect(() => {
// Effect
return () => {
// Clean up effect
};
}, [input]);
// Some Code
return (
//Some JSX
)
}
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 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 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>
</>
)
};
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 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.
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>
)
}
<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?
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.
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.
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.
Plusieurs librairies existent pour nous aider à gérer les formulaires (React Hook Form étant la plus utilisée).
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 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 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 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 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.
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.
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.
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;
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;
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"
},
...
}
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'],
},
});
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);
});
});
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:
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()
})
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.
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
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
Le store (état de notre application) est un simple objet javascript:
{ 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.
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.
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.
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
// 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.
// 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.
// 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
}
}
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:
// 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:
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.
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:
Pour résoudre notre problème, on va utiliser le middleware: 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
// 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).
// 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))
}
}
// 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 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 {
// ...
}
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>
);
}
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
Functional Component
const Learn = () => {...}
Utilise "this" pour accèder au state/props:
this.state.someVariable
this.props.someVariable
Accède directement aux props:
someState
props.someVariable
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
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) !
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...
// 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.
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
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.
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)
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)
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.