📣 Vous trouverez ici les commentaires de ceux qui ont déjà fait le tuto (en attendant leur intégration au tuto)
Le but de ce tutoriel est de découvrir react en construisant une application.
ℹ️ Ce tutoriel n'utilise pas de solution automatique telle que create-react-app (https://fr.reactjs.org/docs/create-a-new-react-app.html).
-
Créer un répertoire à la racine du projet et se placer dedans
-
Télécharger et installer nodejs : https://nodejs.org/en/download/
👉 Vous pouvez maintenant utiliser le gestionnaire de modules de NodeJs npm (https://www.npmjs.com/).
-
Exécuter
npm init
, répondre aux questions ou laisser vide (configuration par défaut)
👉 Un fichier package.json a été créé
4. Installer les modules suivants avec la commande npm install <nom_module>
(ou npm i
):
* webpack webpack-cli webpack-dev-server
* babel-loader babel-preset babel-preset-react @babel/core
* typescript typescript-preset (installer typescript en global (=disponible pour plusieurs projets) `npm install -g typescript`)
* react react-dom @types/react @types/react-dom
👉 un répertoire node_modules est créé qui les contient
ℹ Babel est un transpileur : il permet d'utiliser les dernières implémentations de javascript même si la version du navigateur client ne le permet pas encore
- Créer un fichier babel.config.json et entrer :
{ "presets": [ ["@babel/preset-env", { "targets": { "browsers": ["last 2 chrome versions"] }, "useBuiltIns": "usage" } ], "@babel/preset-typescript", "@babel/preset-react" ] }
ℹ Webpack est un bundler : il regroupe les fichiers javascript en un seul pour optimiser leur utilisation. Alternatives : gulp makefiles, parcel, rollup
- Créer un fichier webpack.config.js et entrer :
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'arolla-react-example.bundle.js',
sourceMapFilename: "todolist.js.map"
},
resolve: { extensions: ['.js', '.ts', '.tsx']},
devtool: "source-map",
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' },
{ test: /\.tsx?$/, exclude: /node_modules/, use: { loader: "babel-loader"} }
]
}
};
ℹ path: path.resolve(__dirname, 'dist'), filename: 'arolla-react-example.bundle.js'
= le bundle nommé _
arolla-react-example.bundle.js_ sera dans le répertoire dist
ℹ resolve: { extensions: ['.js', '.ts', '.tsx']}
= extensions de fichiers acceptées .js, .ts, .tsx
ℹ { test: /\.tsx?$/, exclude: /node_modules/, use: { loader: "babel-loader"} }
= si mon fichier est un .tsx le traiter avec babel
-
Créer un dossier dist ( = distribution) vide
ℹ le bundle sera créé dedans lors du build
-
Créer un fichier index.html, ajouter au moins une balise avec un id et une balise script pointant vers le bundle
<div id="projet"></div>
<script src="./dist/arolla-react-example.bundle.js"></script>
ℹ Le div#projet est destiné à contenir l'application
- Dans le package.json, ajouter une ligne dans scripts
"build": "webpack"
:
{
"name": "arolla",
"version": "1.0.0",
"description": "exemple de projet react-redux",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
},
"author": "mathilde",
"license": "ISC",
"dependencies": {
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.2",
"babel-preset": "^1.1.7",
"babel-preset-react": "^6.24.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript-preset": "^1.0.0",
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
}
}
ℹ La commande sera lancée pour build le projet
- Définir un port : Ajouter une ligne dans le webpack.config.js entre output et resolve
devServer: {
port: 5500,
},
- Ajouter une ligne dans le package.json
"scripts" {
"serve": "webpack serve --mode=development"
}
ℹ La commande sera exécutée pour lancer le projet
- Créer un répertoire src
- Dedans, ajouter un fichier App.tsx et entrer :
import React from 'react';
const App : React.FC = () => (
<div>Hello !</div>
);
export default App;
ℹ Ce sera notre composant principal racine de tous les autres
ℹ L'extension .tsx accepte le html et le typescript
- et un fichier index.tsx et entrer :
import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
const container = document.getElementById('projet');
ReactDom.render(<App/>, container);
ℹ Ce sera le fichier principal du projet comme l'indique la ligne "main": "index.js"
du package.json
et entry: './src/index.tsx',
dans webpack.config.js.
ℹ Le container ici est la balise ajoutée dans index.html.
- Builder le projet
npm run build
- Lancer le serveur
npm run serve
- Dans un navigateur, aller à la page http://localhost:5500 et voyez "Hello !"
-
Lancer la commande
tsc --init
à la racine du projet (Penser à mettre à jour les variables d'environnement)👉 cela créera un fichier tsconfig.json (ou bien créez-le)
-
Mettre la ligne à true :
"esModuleInterop": true,
Nous allons créer une liste (de choses à faire, de challenges, d'ingrédients de cuisine...selon votre humeur), soit 2 composants :
- un formulaire pour ajouter un élément à la liste
- un composant pour afficher la liste
- Créer un répertoire todolist
- Dedans, créer un fichier ItemCreationComponent.tsx : le formulaire d'ajout
- Dedans, créer un fichier ListDisplayComponent.tsx : la liste affichée
Voici les deux manières de faire, en faire un composant de chaque style :
- Créer une nouvelle fonction dans le fichier ItemCreationComponent.tsx :
const ItemCreationComponent = () => { };
- ...qui retourne le html du formulaire :
import React from 'react';
const ItemCreationComponent: React.FC = () => {
return (<form id="todolist">
<label htmlFor="item">Je dois faire : </label>
<input type="text" name="item"></input>
<button type="submit"> ok </button>
</form>);
};
}
- ...et l'exporter :
export default ItemCreationComponent;
- ...puis l'importer dans le composant principal App.tsx (créé à l'étape 1) :
import React from 'react';
import ItemCreationComponent from './todolist/ItemCreationComponent';
const App : React.FC = () => (
<ItemCreationComponent/>
);
- Lancer l'application comment faire ?
- Créer une nouvelle classe dans le fichier ListDisplayComponent.tsx :
class ListDisplayComponent {
// ....
}
- ...qui étend l'interface React.Component et implémente la fonction render() :
import React from 'react';
class ListDisplayComponent extends React.Component {
render() {
return <div id="listOfItems">
emplacement pour ma future liste
</div>;
};
}
- ...et l'exporter :
export default ListDisplayComponent;
- ...puis l'importer dans le composant principal App.tsx (créé à l'étape 1) :
import React from 'react';
import ItemCreationComponent from './todolist/ItemCreationComponent';
import ListDisplayComponent from './todolist/ListDisplayComponent';
const App : React.FC = () => (
<div>
<ItemCreationComponent/>
<ListDisplayComponent/>
</div>
);
-
Lancer l'application comment faire ?
👉 Vous devez voir apparaître vos deux composants.
Les deux composants que vous avez créés ne sont pas fonctionnels. Nous allons voir dans la prochaine étape comment ajouter du comportement aux composants et les faire communiquer entre eux.
Voici le comportement attendu :
- On entre un texte dans le champ
- On clique "ok"
- Le texte entré est affiché en dessous du formulaire
- Tant qu'on ne rafraichit pas la page, toutes les entrées s'ajoutent sous forme de liste
- Ouvrir ItemCreationComponent.tsx et ajouter :
- une fonction addItem appelée lors de la soumission du formulaire (au clic du bouton "ok" de type submit) (onSubmit)
- une fonction setItemValue appelée lorsque la valeur du champ change (onChange)
Ces deux méthodes ont un paramètre implicite event (interface Event). Il représente les événements déclenchés dans le navigateur tels que les clics ou les soumissions de formulaire.
a. Si vous avez créé ce composant en tant que fonction
import React from 'react';
const ItemCreationComponent: React.FC = () => {
return (
<form id="todolist" onSubmit={ addItem }>
<label htmlFor="item">Je dois faire : </label>
<input type="text" name="item" onChange={ setItemValue }/>
<button type="submit"> ok</button>
</form>
);
}
const addItem = () => (event: Event) => { event.preventDefault(); };
const setItemValue = () => (event: Event) => { };
export default ItemCreationComponent;
ℹ event.preventDefault()
est présent dans notre exemple pour éviter de réellement soumettre le formulaire, ce qui
aurait pour effet de rafraîchir la page et de remettre notre liste à l'état initial (vide). En effet, dans notre
exemple, il n'existe pas de sauvegarde des éléments de liste que nous ajoutons.
b. Si vous avez créé ce composant en tant que classe :
import React from 'react';
class ItemCreationComponent extends React.Component {
constructor(props: any) {
super(props);
this.setItemValue = this.setItemValue.bind(this);
this.addItem = this.addItem.bind(this);
}
addItem(event: Event) { event.preventDefault(); };
setItemValue(event: Event) { };
render() {
return <form id="todolist" onSubmit={ this.addItem }>
<label htmlFor="item">Je dois faire : </label>
<input type="text" name="item" onChange={this.setItemValue }/>
<button type="submit"> ok</button>
</form>;
};
}
export default ItemCreationComponent;
☝ Pourquoi this.setItemValue.bind(this);
est nécessaire ? Qu'est-ce que this ?
this représente le contexte d'exécution d'une fonction. C'est un paramètre dynamique : il change en fonction de
l'endroit d'où est appelée la fonction.
this.setItemValue.bind(this);
permet de redéfinir ce contexte pour setItemValue afin qu'il corresponde à la classe _
ItemCreationComponent_.
☝ Peut-on rendre le bind implicite ?
Oui, en ayant recours aux fonctions fléchées (arrow functions)
soit
addItem = () => (event: Event) { event.preventDefault(); };
soit
render() {
return <form id="todolist" onSubmit={ () => this.addItem() }>
...
</form>;
};
plus de détails sur this dans You don't know JS v.2 (voir Closure et this Keyword)
plus de détails sur bind dans une classe de composant React ici
L'idée de base est de gérer un état local des variables utilisées par le composant.
En tant que classe
2.a. Commencer par définir la structure du state à l'aide d'une interface (State). Dans le constructeur, on lui donne un état initial. this.state servira à stocker l'état de l'élément de liste que nous voulons ajouter.
interface State {
item: string
};
class ItemCreationComponent extends React.Component<{}, State> {
constructor(props: any) {
//...
this.state = { item: '' };
//...
}
// ...
}
ℹ this.state doit respecter la structure définie par l'interface State
3.a. Implémenter setItemValue pour que state recueille la valeur entrée par l'utilisateur :
il faut utiliser setState qui selon la documentation de React :
planifie des modifications à l’état local du composant, et indique à React que ce composant et ses enfants ont besoin d’être rafraîchis une fois l’état mis à jour
class ItemCreationComponent extends React.Component<{}, State> {
//...
setItemValue(event: Event) {
if (event) {
const fieldValue: string = event.target.value;
this.setState({item : fieldValue});
}
};
//...
}
ℹ event.target.value est la valeur entrée par un utilisateur dans le champ
ℹ Le paramètre de setState doit respecter la structure définie par l'interface State
En tant que fonction
2.b. Pour initialiser le state, on doit utiliser un hook pour bénéficier des fonctionnalités de React.
Le hook useState :
- permet de définir l'état initial
- renvoie l'état local (ici state) et la méthode pour le mettre à jour (ici setState)
import React, { useState } from 'react';
interface State {
item: string
};
const ItemCreationComponent: React.FC = () => {
const [state, setState] = React.useState<State>({ item: '' });
// ...
}
ℹ Cette syntaxe const [item, setItem] = React.useState<State>({ item: '' });
est une déstructuration.
userState renvoie un tableau à deux cellules, dans la première un état et dans la deuxième la fonction pour le mettre à jour.
Par ce procédé, il est affecté la valeur de la cellule 1 à la variable item et la valeur de la cellule 2 à la variable setItem
au lieu d'affecter dans une variable le tableau entier comme çà const toutLeTableau = React.useState<State>({ item: '' });
.
3.b. Implémenter setItemValue pour que state recueille la valeur entrée par l'utilisateur :
const ItemCreationComponent: React.FC = () => {
const [state, setState] = React.useState<State>({ item: '' });
return <form id="todolist" onSubmit={ addItem }>
//...
<input type="text" name="item" onChange={ setItemValue(setState) }/>
// ...
</form>;
}
const setItemValue = (setItem) => (event: Event) => {
if (event) {
const fieldValue: string = event.target.value;
setItem(fieldValue);
}
};
ℹ ni state, ni setState ne sont accessibles à setItemValue car elles sont définies dans le scope de la fonction ItemCreationComponent. Le scope est l'ensemble des règles qui régissent comment les références aux variables sont résolues. C'est pourquoi il faut passer la fonction setItem en paramètre de setItemValue.
🎗️ Rappel : App.tsx
Le composant formulaire de création d'élément et le composant d'affichage de liste ont un parent racine commun (App).
const App: React.FC = () => (
<div>
<ItemCreationComponent/>
<ListDisplayComponent/>
</div>
);
C'est grâce à lui qu'il vont communiquer.
- Le composant d'affichage de liste doit recevoir une liste d'éléments à afficher.
Ouvrir ListDisplayComponent.tsx et définir une interface pour les paramètres reçus lors de la création du composant. Elle va contenir une liste d'éléments. Elle sera nommée Props.
interface Props {
items: string[]
};
- Ajouter les props comme suit :
En tant que classe
class ListDisplayComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
props = { items : [] }
}
//...
}
En tant que fonction
const ListDisplayComponent : React.FC<Props> = ({items = []}) => {
//...
}
ℹ ici les paramètres sont déstructurés.
Si on écrit const ListDisplayComponent : React.FC<Props> = (props) => {}
alors props contient un objet avec un tableau d'items suivant la structure de l'interface Props.
Au contraire, si on écrit const ListDisplayComponent : React.FC<Props> = ({items = []}) => {}
alors items contient directement le tableau d'items, initialisé à vide = []
s'il n'est pas déjà défini.
- Pour afficher les éléments, ajouter dans la balise div
En tant que classe
class ListDisplayComponent extends React.Component<Props> {
//...
render() {
return <div id="listOfItems">
emplacement pour ma future liste
{
this.props.items.map((item: string) => {
return (<p> - {item}</p>);
})
}
</div>;
};
}
ℹ grâce à map() la même opération est appliquée à chacun des éléments d'une liste. Ici les afficher.
En tant que fonction
const ListDisplayComponent : React.FC<Props> = ({items = []}) => {
return <div id="listOfItems">
emplacement pour ma future liste
{
items.map((item: string) => {
return (<p> - {item}</p>);
})
}
</div>;
}
- Le composant-formulaire doit rendre disponibles les valeurs entrées par l'utilisateur au composant.
Ouvrir ItemCreationComponent.tsx et implémenter la méthode addItem.
En tant que fonction
interface Props {
onAddItem: (item: string) => void;
}
const ItemCreationComponent: React.FC<Props> = ({ onAddItem }) => {
const [state, setState] = React.useState<State>({ item: '' });
return <form id="todolist" onSubmit={ addItem(state, onAddItem) }>
//...
</form>;
}
const onSubmitForm = (state: State, onAddItem) => (event) => {
event.preventDefault();
if (onAddItem) {
onAddItem({ item: state.item });
}
};
En tant que classe
interface Props {
onAddItem: (item: string) => void;
}
class ItemCreationComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { item: '' };
//...
}
addItem(event: Event) {
event.preventDefault();
if ( this.props.onAddItem) {
this.props.onAddItem({ item: this.state.item });
}};
//...
}
...
- La communication entre ces composants se fait grâce au composant parent commun.
Ouvrir App.tsx et compléter comme suit :
interface State {
items: string[]
};
const App: React.FC = () => {
const [state, setState] = React.useState<State>({items: [] });
return <div>
<ItemCreationComponent onAddItem = { onAddItem(state, setState) }/>
<ListDisplayComponent items={ state.items }/>
</div>
};
const onAddItem = (state, setState) => (userEntry) => {
if (userEntry) {
setState({items: [...state.items, userEntry.item] });
}
}
- Le composant parent App maintient dans son état local (state) une liste d'éléments (items: string[]).
- Le composant-formulaire ItemCreationComponent met à jour cette liste en ajoutant au fur et à mesure les éléments lorsqu'ils sont entrés par l'utilisateur via le formulaire (onAddItem).
- Le composant d'affichage ListDisplayComponent reçoit la liste de son parent en paramètre
items={ state.items }
.
ℹ userEntry est envoyé par ItemCreationComponent dans via la fonction addItem.
ℹ Dans cette syntaxe [...state.items, userEntry.item]
les points de suspension sont un spread operator.
Grâce à cet opérateur, un nouveau tableau est créé contenant tous les éléments présents dans state.items
auxquels s'ajoute la nouvelle valeur userEntry.item.
-
Lancer l'application comment faire ?
👉 Les composants sont maintenant opérationnels. Les entrées du formulaire sont affichées dans une liste en dessous.
👨👩👧👦 À mesure qu'une application grossit, le nombre de ses composants devient conséquent et leur hiérarchie en vient à comporter plusieurs niveaux de profondeur. La communication entre composants parent/enfant ou entre composants-frères devient fastidieuse et complexe à orchestrer.
Une solution peut être de maintenir un état global des données à partager entre les composants en ayant recours à un store.
Le store va garder l'état des variables nécessaires à plus d'un composant à jour. À chaque mise à jour de l'une d' entre elles, tous les composants reliés au store qui utilisent cette variable auront accès à sa nouvelle valeur.
- Installer le store redux.js
npm install <nom_module>
- react-redux
- redux
- @types/react-redux
- redux-thunk
- Créer un fichier ToDoReducer.ts dans un répertoire src/reducers
import { Reducer } from 'redux';
const ToDoReducer: Reducer = (state, action) => {
return state;
};
export default ToDoReducer;
ℹ un reducer a pour paramètres l'état courant de l'application (state) et une action requise sur cet état (comme une mise à jour)(action). Les changements de l'état demandés seront implémentés dedans : le nouvel état (state) est ensuite renvoyé.
- Créer un fichier Store.ts dans /src
import { createStore } from 'redux';
import { ToDoReducer } from './reducers/ToDoReducer';
const store = createStore(ToDoReducer);
export default store;
ℹ un store est créé avec au moins un reducer associé.
Il est possible d'avoir plusieurs stores et plusieurs reducers. Il est important de bien réfléchir avant de prendre la décision de découper. Dois-je créer des stores multiples ?.
Voici un exemple avec deux reducers : un pour une todolist et un pour un système de gestion d'utilisateurs :
import { AnyAction, combineReducers, createStore, Store } from 'redux';
export type AppState = {todos: string[], users: UserModel[]};
// on combine les reducers
const rootReducer = combineReducers({todos: todoReducer, users: userReducer});
// et la combinaison est fournie au store
const store: Store<AppState, AnyAction> = createStore(rootReducer);
documentation de combineReducers >>
- Ouvrir Index.tsx :
//...
import {Provider} from 'react-redux';
import store from './Store';
const container = document.getElementById('projet');
// cette ligne remplace ReactDom.render(<App/>, container);
ReactDom.render(
<Provider store={store}>
<App/>
</Provider>,
container
)
ℹ Maintenant nous avons configuré un store au dessus du composant racine. Les composants devront s'y connecter pour l' utiliser.
- Créer un fichier ToDoListActions.ts pour y définir les actions possibles sur la liste. Y ajouter la possibilité d'une action d'ajout d'un élément dans la liste. Cette action sera identifiable via l'étiquette 'ADD_TO_DO' et aura un paramètre todo, une chaîne de caractères qui représente l'élément à ajouter à la liste.
import {Action} from "redux";
export type ADD_TO_DO = 'ADD_TO_DO';
export type AddTodoAction = {
todo: string
} & Action<ADD_TO_DO>;
const addTodo = (todo: string): AddTodoAction => ({
type: 'ADD_TO_DO',
todo
});
export default {addTodo};
ℹ En typescript, le mot clé type permet de définir des alias pour des types afin de les réutiliser.
ℹ & permet de créer un type à partir de deux types :
celui qui a cette structure :
{
todo: string
}
ET Action<ADD_TO_DO> qui correspond l'interface suivante
export interface Action<T = any> {
type: T
}
s'ajoutent pour former le type AddTodoAction. La constante addTodo est de type AddTodoAction
- Compléter le fichier ToDoReducer.ts comme suit pour ajouter un élément donné dans la liste stockée dans le _ store_ :
import {Reducer} from 'redux';
import {AddTodoAction} from './ToDoListActions';
const initialState = { todos: [] };
export const ToDoReducer: Reducer<string[], AddTodoAction> = (state = initialState.todos, action) => {
switch (action.type) {
case 'ADD_TO_DO':
return [...state, action.todo];
default:
return state;
}
};
ℹ Cette syntaxe (state = initialState.todos)
dans le cas d'un paramètre d'entrée sert à assigner une valeur par défaut.
On va stocker dans le store la liste des choses à faire (todos).
A l'état initial, la liste est vide const initialState = { todos: [] };
.
ℹ Lorsque le reducer reçoit une action un nouvel état du store est retourné.
Si c'est une action de type ADD_TO_DO, il est renvoyé une nouvelle liste contenant
le nouvel élément ajouté. Cette syntaxe (spread operator)[...state, action.todo];
permet de créer la nouvelle liste à partir d'une copie de l'ancienne.
Cela garantit que Les données sont immuables. Cela rend les modifications apportées sur le dom plus prévisibles
et évite les effets de bords. Contrairement aux objets ou aux arrays, les types primitifs (boolean, string, number..) en javaScript sont immuables .
>> comprendre l'intérêt des données immuables
ℹ Les exemples ci-après sont donnés uniquement pour les composants créés en tant que fonction
-
Ouvrir Store.ts. Définir la structure du store. Dans notre exemple, nous allons stocker la liste des todos.
Ajouter cette ligne :
export type AppState = { todos: string[] };
et compléter celle-ci :
const store: Store<AppState> = createStore(ToDoReducer);
- Ouvrir ListDisplayComponent.tsx. La fonction Connect permet à un composant de se connecter à un store comme ceci :
import {connect} from 'react-redux';
//remplace la ligne : export default ListDisplayComponent
export default connect(mapStateToProps, null)(ListDisplayComponent);
Elle prend en paramètre le composant ListDisplayComponent et en retourne un nouveau "connecté au store".
Quant au paramètre mapStateToProps il est utile pour recevoir les mises à jour du store
Le définir comme ceci :
import {AppState} from '../../Store';
const mapStateToProps = (state: AppState) => {
return {
todos: state || []
}
}
ℹ la syntaxe state.todos || []
permet d'initialiser todos avec un array vide si state.todos était indéfini.
- Ouvrir ItemCreationComponent.tsx. Ajouter la fonction connect comme ceci :
import {connect} from 'react-redux';
//remplace la ligne : export default ItemCreationComponent
export default connect(null, mapDispatchToProps)(ItemCreationComponent);
mapDispatchToProps est utile pour déclencher des actions sur le store (dispatch)
Le définir comme ceci :
import actionsCreator from '../../reducers/ToDoListActions';
const mapDispatchToProps = (dispatch) => {
return {
addItem: (todo: string) => {
dispatch(actionsCreator.addTodo(todo))
},
}
}
🎗️ Rappel : addToDo retourne une action de cette forme :
{
type: 'ADD_TO_DO'
todo: string
}
Cette ligne dispatch(actionsCreator.addTodo(todo))
peut être vue comme un envoi de la consigne 'ADD_TO_DO'
accompagnée de la valeur de l'élément à ajouter (todo).
Pour terminer,
- Renommer onAddItem en addItem dans l'interface Props
- Modifier la fonction onAddItem définie dans le tutoriel 1 pour appeler addItem à la soumission du formulaire comme suit (OnSubmit):
//...
interface Props {
addItem: (item: string) => void;
}
//...
const ItemCreationComponent: React.FC<Props> = ({addItem}) => {
//...
return <form id="todolist" onSubmit={onAddItem(state, addItem)}>
//...
</form>;
//...
const onAddItem = (state, addItem) => (event: Event) => {
event.preventDefault(); //pour ne pas soumettre le formulaire et rafraichir la page
if (state && state.item && addItem) {
addItem(state.item);
}
};
}
- Enfin, ouvrir App.tsx. Et supprimer les lignes inutiles héritées du tutoriel 1 pour obtenir :
const App: React.FC = () => {
return <div>
<ItemCreationComponent/>
<ListDisplayComponent items={[]}/>
</div>
};
export default App;
- Lancer l'application comment faire ? 👉 Le comportement attendu est le même qu'à la fin du tutoriel 1. Les entrées du formulaire sont affichées dans une liste en dessous.
Redux DevTools est une extension qui permet de contrôler l'état du store directement dans le navigateur.
Voici un exemple de configuration : (pour Chrome)
- Recherche Redux DevTools dans le Chrome web store et ajouter l'ajouter à son navigateur
- Suivre les instructions données ici
Ce qui revient à ouvrir Store.ts et à ajouter 'REDUX_DEVTOOLS_EXTENSION' dans cette ligne :
//...
const store: Store<AppState> = createStore(ToDoReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
//...
- Ouvrir l'onglet Redux dans les outils de developpement de Chrome (ctrl + maj + i) pour voir les changements d'états du store
️ Dans notre exemple simplissime, la liste des choses à faire est initialement vide. Mais il arrive souvent que des éléments soient ajoutés à une liste déjà existante. Pour similer ce cas, nous allons charger des données depuis un fichier pour peupler notre liste.
- Ajouter un repertoire resources et créer un fichier myTodoList.json contenant une liste d'élements pour la liste :
[
"faire la vaisselle",
"acheter des pommes",
"réparer l'étagère"
]
- Pour récupérer des données d'une ressource, nous utiliserons l'API fetch:
Promise < Response > fetch(entrée [, paramètres]);
☝ Qu'est ce qu'une Promise ?
Pour comprendre cela, il faut d'abord comprendre ce qu'est l'asynchronisme (opposé au synchronisme). Javascript étant essentiellement single-thread, c'est-à-dire qu'il n'y a qu'un seul fil d'exécution chargé de dérouler une à une les opérations planifiées, le recours à l'asynchronisme permet de différer l'exécution d'une tâche, à un moment où elle est le moins susceptible de causer des blocages ou des ralentissements entraînant une mauvaise expérience utilisateur et des problèmes de performance.
Une Promise (promesse) représente le résultat d'une opération asynchrone éventuellement disponible dans le futur. En effet, cette opération peut échouer, dans ce cas il est retourné la cause de l'échec, ou réussir, dans ce cas, la valeur du résultat est accessible dès que la tâche est complétée.
en savoir plus sur l'asynchronisme et les promises >>
Dans notre exemple le code suivant pourrait être utilisé pour obtenir la liste de todos :
// then pour obtenir le résultat d'une promise
// fetch() et json() renvoient toutes les deux une Promise
// il y a donc une chaine d'opérations
const todos = fetch('resources/myTodoList.json').then(result => result.json()).then(result => result);
Une autre syntaxe possible serait d'utiliser l'opérateur await, pour indiquer d'attendre la résolution de la Promise pour renvoyer son résultat.
const response = await fetch('resources/myTodoList.json');
const todos = await response.json();
🛑✋ Il reste cependant un obstacle: Un store Redux n'accepte pas les actions asynchrones car il n'accepte aucune action pouvant entrainer des effets de bords.
Les effets de bords ce sont tous les changements de l'état d'une application survenus en dehors de son contexte initial, comme par exemple, une fonction qui modifie une variable qu'elle a reçu en paramètre, un appel à une API externe ou encore la génération de nombres aléatoires. (Autrement dit, pas de changement du store en dehors de son contexte)
☝ Que va faire le middleware ?
Le middleware va intercepter la demande d'action en amont du reducer, réaliser une opération entrainant des effets de bord avant de redistribuer(dispatch) l'action à destination du reducer avec éventuellement le résultat de l'opération réalisée en paramètre.
- Ouvrir Store.tsx et compléter comme suit pour ajouter le middleware redux thunk qui va gérer la logique asynchrone :
import {createStore, Store, applyMiddleware} from 'redux';
import {ToDoReducer} from './reducers/ToDoReducer';
import thunkMiddleware from 'redux-thunk'
export type AppState = { todos: string[] };
const enhancer = applyMiddleware(thunkMiddleware);
const store: Store<AppState> = createStore(ToDoReducer, enhancer);
export default store;
ℹ Un enhancer est un moyen d'ajouter des options de configuration du store
ℹ Nous avions précédemment intégré devTools, voici comment faire pour le garder :
a. installer redux-devtools-extension
npm install --save-dev redux-devtools-extension
b. puis configurer comme suit
//...
import { composeWithDevTools } from 'redux-devtools-extension';
//...
const middleware = applyMiddleware(thunkMiddleware);
const enhancers = composeWithDevTools(middleware);
const store: Store<AppState> = createStore(ToDoReducer, enhancers);
//...
- Ouvrir AddTodoAction.tsx et créer un nouveau type d'action de type initialisation de liste :
// ...
export type INIT_TO_DO_LIST = 'INIT_TO_DO_LIST' ;
export type ListTodoAction = {
todos: string[]
} & Action<INIT_TO_DO_LIST>;
const fetchTodos = (): ListTodoAction => ({
type: 'INIT_TO_DO_LIST',
todos: []
});
export default {addTodo, fetchTodos};
- Ouvrir ToDoReducer.tsx et ajouter la fonction fetchTodos :
// charge la liste en asynchrone
export const fetchTodos = () => async (dispatch, getState) => {
const response = await fetch('resources/myTodoList.json');
const todos = await response.json();
dispatch({type: 'INIT_TO_DO_LIST', todos: todos});
}
puis faire en sorte que la nouvelle action 'INIT_TO_DO_LIST' puisse être traîtée par le reducer :
import {AddTodoAction, ListTodoAction} from './ToDoListActions';
type todoActions = AddTodoAction | ListTodoAction;
export const ToDoReducer: Reducer<string[], todoActions> = (state = initialState.todos, action ) => {
switch (action.type) {
case 'ADD_TO_DO':
return [...state, action.todo];
case 'INIT_TO_DO_LIST':
return action.todos; // renvoie la liste chargée du fichier
default:
return state;
}
};
- Enfin, dans le composant ListDisplayComponent.tsx compléter comme suit :
Ajouter la possibilité de déclencher le chargement (initList)
import {fetchTodos} from "../../reducers/ToDoReducer";
interface Props {
items: string[];
initList: (() => string[]);
}
const mapDispatchToProps = (dispatch) => {
return {
initList: () => { dispatch(fetchTodos()) }
}
}
const ListDisplayComponent: React.FC<Props> = ({items = [], initList }) => {
//...
}
export default connect(mapStateToProps, mapDispatchToProps)(ListDisplayComponent);
Puis faire l'appel de la méthode dans un hook d'effet :
//...
const ListDisplayComponent: React.FC<Props> = ({items = [], initList }) => {
React.useEffect(() => {
initList();
}, []);
//...
}
☝ j'ai oublié ce qu'est un hook...Regarder tutoriel 1 : le hook d'état useState
ℹ Le hook d'effet useEffect autorise les effets de bords, dans notre cas, il permet de modifier l'état de la liste après son initialisation à vide en la peuplant des valeurs du json.
Le 1er paramètre de useEffect est l'opération voulue (un effet).
Le 2nd paramètre facultatif, est une liste permettant de connaitre le bon moment du déclenchement de cette opération
(à défaut après chaque affichage). Dans cet exemple, la liste ne sera mise à jour
que si l'état précédent de celle-ci est vide ([]
).
En savoir plus sur Le hook d'effet useEffect
- Lancer l'application comment faire ?
👉 A l'affichage, la liste affiche déjà les éléments contenus dans le json. Quand on entre un nouvel élément via le formulaire, il s'ajoute à la liste sans que les précédents éléments ne disparaissent.