TP1 - Todolist - Partie 1
#
Intro#
Avant de commencerLors de ce TP, nous allons créer une petite application de gestion de tâches (alias todos), que vous pouvez d'ores et déjà tester sur cette page. L'objectif est de prendre ses marques avec React et d'en saisir les principes fondamentaux.
Vous ĂŞtes tout Ă fait libre de chercher des ressources sur les sites que vous souhaitez, mais je vous recommande quand mĂŞme ces quelques ressources :
Ce TP est volontairement très guidé, mais ce ne sera pas le cas des suivants, je vous encourage donc fortement à le faire rigoureusement :
- Ne regardez pas les corrections avant d'avoir essayé par vous même
- N'hésitez pas à poser une question durant le TP avant de regarder la correction si vous êtes bloqué
#
Pré-requisIl faut toutefois remplir un certain nombre de pré-requis avant de pouvoir commencer le TP :
- Connaître les fondamentaux de HTML, CSS et Javascript
- Avoir Git installé
- Avoir une clé SSH enregistrée sur le Gitlab de l'université
- Avoir Nodejs et NPM installés sur votre poste
#
Mise en place#
Projet GitDans un premier temps, il vous faut cloner le projet contenant le code de départ sur votre poste, et enlever la référence au repository d'origine. Vous pouvez le faire à l'aide des commande suivantes :
# On clone le projet de départgit clone https://gitlab.com/lp-miaw-react/react-todo-app.gitcd react-todo-app# On enlève la distante originalegit remote remove origin
Il vous faut ensuite créer un nouveau projet sur le Gitlab de l'université que vous pourrez nommer tp1-react-todolist
.
Ne cochez pas la case vous proposant de faire un commit initial.
Copiez le lien de clone de votre repository à l'aide du bouton bleu "Clone" et collez le à l'endroit approprié de la commande suivante pour pouvoir y envoyer du code.
# On ajoute l'adresse de votre projet comme distantegit remote add origin <your-project-url>
Vous devriez maintenant pouvoir pousser le code vers Gitlab en utilisant cette commande :
git push -u origin main
#
Dépendances et serveur de développementDans un premier temps, il nous faut installer les dépendances de notre projet. Pour ce faire, il suffit de taper la commande suivante dans un terminal situé dans le dossier de notre projet.
npm install
Une fois l'installation terminée, on peut lancer la compilation et le serveur de développement `a l'aide de la commande suivante :
npm run start
#
Code de départVous travaillerez lors de ce TP uniquement sur les fichiers situés dans le dossier components
.
Ouvrez le fichier App.jsx
. Comme vous pouvez le constater, il contient tout le code pour l'application que vous avez pu observer Ă l'instant.
Examinez attentivement le code d'origine dans votre éditeur de code, et utilisez l'inspecteur de votre navigateur pour identifier les différents éléments de l'application.
#
Découper en composantsLe concept fondamental de React est de séparer les éléments d'une interface en morceaux réutilisables de code. Pour ce faire, nous allons identifier un premier élément que nous pourrons réutiliser : l'élément de liste qui représente un "todo".
TodoItem
#
Le composant Extrayez cet élément dans un nouveau fichier components/TodoItem.jsx
.
Correction
import React from 'react'
const TodoItem = props => {
return <li className="completed"> <div className="view"> <input className="toggle" type="checkbox" defaultChecked /> <label>Tester React</label> <button className="destroy" /> </div> <form> <input className="edit" defaultValue="Tester React" /> <input type="submit" value="Valider" className="hidden" /> </form> </li>}
export default TodoItem
Form
#
Le composant Une autre partie de notre code, même si elle ne sera pas réutilisée, peut être extraite pour la retravailler plus tard : le formulaire d'ajout de tâche.
Essayez donc de l'extraire vous mĂŞme dans un fichier components/Form.jsx
.
Correction
import React from 'react'
const Form = props => { return <form> <input className="new-todo" placeholder="Qu'avez vous Ă faire ?" autoFocus /> <input className="hidden" type="submit" value="Ajouter" /> </form>}
export default Form
Une fois ceci terminé, faites un commit avec le commentaire "Ex 1 : Splitting components" et poussez vers le serveur.
git add .git commit -m 'Ex 1 : Splitting components'git push
props
#
Passer des Maintenant que nous disposons d'un composant TodoItem
séparé, utilisons le dans le code de notre composant App
:
<ul> <TodoItem /> <TodoItem /> <TodoItem /></ul>
name
#
Passer Les éléments s'affichent bien dans la liste, cependant... Ils portent tous le même nom !
Modifions notre composant pour qu'il puisse afficher un nom différent.
<TodoItem name="A faire 1" /><TodoItem name="A faire 2" /><TodoItem name="A faire 3" />
complete
#
Passer Nous pouvons maintenant changer le nom d'un todo, mais pas encore son statut.
Ajoutons donc une prop complete
Ă notre composant :
<TodoItem name="A faire 1" complete={true} /><TodoItem name="A faire 2" complete={false} /><TodoItem name="A faire 3" complete={false} />
Les éléments input de type checkbox (entre autres) utilisent une prop
checked
pour gérer leur état.
id
#
Passer Vous l'aurez certainement remarqué : dans la console Javascript du navigateur, une erreur est affichée au sujet de la propriété id
de notre input, celle la même qui est référencée par notre label. Effectivement, celle-ci est sensé être unique au sein du DOM.
Ajoutez donc un moyen de la passer au composant TodoItem
depuis le composant App
de la sorte :
<TodoItem name="A faire 1" complete={true} id="todo-1" /><TodoItem name="A faire 2" complete={false} id="todo-2" /><TodoItem name="A faire 3" complete={false} id="todo-3" />
Une fois ceci terminé, faites un commit avec le commentaire "Ex 2 : Passing props" et poussez vers le serveur.
git add .git commit -m 'Ex 2 : Passing props'git push
#
Liste en donnée et itérationNous arrivons maintenant à configurer complètement l'affichage de nos tâches sans répéter de code à l'aide d'un composant. Cependant, il serait bien plus pratique de pouvoir en afficher un nombre indéterminé à l'aide d'une variable de type Array
, comme celle ci par exemple :
const todoList = [{ id: 'todo-1', name: 'Tester React', completed: true}, { id: 'todo-2', name: 'Terminer le TP', completed: true}, { id: 'todo-3', name: 'Offrir du saucisson au prof', completed: false}]
Il se trouve que React sait afficher plusieurs types de variables différents, dont des chaines de caractères, des nombres ou des éléments React, mais aussi des Array
de chacun de ces types.
Il nous faut donc transformer notre Array
d'objets en Array
d'élement React (en l'occurrence, des TodoItem
).
Il existe en Javascript une fonction Array#map permettant de transformer tous les objets d'un
Array
...
const todoElements = todoList.map(todo => <TodoItem id={todo.id} name={todo.name} complete={todo.complete}/>)
// On peut ensuite afficher ces éléments :return <ul> {todoElements}</ul>
Array#map
étant une expression (c'est à dire qu'elle retourne quelque chose), nous pouvons aussi l'utiliser directement dans le code JSX :
return <ul> {todoList.map(todo => <TodoItem id={todo.id} name={todo.name} complete={todo.complete} />)}</ul>
#
Choisir une cléQuand on affiche une liste, React stocke quelques informations sur chaque élément de liste affiché. Quand on met à jour la liste, React a besoin de déterminer ce qui a changé. On pourrait avoir ajouté, supprimé, ré-arrangé ou mis à jour les éléments de la liste.
Imaginez passer de ceci :
<li>​Alex​a:​ ​7​ tasks ​left​</li><li>​Ben: ​5​ tasks ​left​</li>
Ă€ cela :
<li>​Ben: ​9​ tasks ​left​</li><li>​Claudi​a:​ ​8​ tasks ​left​</li><li>​Alex​a:​ ​5​ tasks ​left​</li>
En plus d'avoir mis a jour les nombres, un humain lisant ceci dirait probablement que nous avons inverse les positions d'Alexa et de Ben et insère Claudia entre Alexa et Ben.
Cependant, React est un programme et n'est pas informé de ce que nous avons l'intention de faire. Comme React ne connaît pas nos intentions, nous devons specifier une clé sous la forme d'une prop key pour chaque element de liste afin de les différencier.
Pour l'exemple donné, une solution pourrait être d'utiliser les chaines de caractères "alexa", "ben" et "claudia". Si nous les affichions depuis une base de donnees, les identifiants uniques en provenance de cette dernière pourraient être utilises comme des clés :
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
Quand une liste est affichée, React prends la clé de chaque élément et cherche dans l'affichage précédent une clé identique. Si la liste actuelle a une clé qui n'existait pas avant, React créé un élément DOM. Si deux clés sont identiques, l'élément correspondant est déplacé. Les clés informent React de l'identité de chaque élément, ce qui permet de réutiliser l'élément du DOM entre deux affichages. Si la clé d'un élément change, cet élément est détruit et recrée.
L'attribut key
est reserve dans React (ainsi que ref
, une fonctionnalité plus avancée).
Quand un élément est créé, React extrait cet attribut et stocke la clé directement dans l'élément retourné. Meme si cela ressemble a une prop, on ne peut pas y acceder en utilisant props.key
. React utilise automatiquement les clés pour decider de quel élément mettre a jour. Un élément ne peut se renseigner sur sa propre clé.
Il est donc fortement recommandé d'assigner une clé propre a chaque élément quand vous construisez des listes dynamiques. Si vous n'avez pas de clé appropriée, vous devriez envisager de de restructurer vos données pour en avoir une.
Si aucune clé n'est spécifiée, React affichera un avertissement et utilisera l'index comme clé par défaut. Cependant, utiliser l'index de la liste comme clé est problématique quand on essaye de réorganiser des éléments de liste, ou en ajouter/retirer.
Passer explicitement l'index comme clé enlève le warning, mais le problème persistera. Dans la plupart des cas, ce n'est pas recommandé. Les clés n'ont pas besoin d'être uniques globalement, elles doivent seulement l'être entre composants issus d'une meme liste.
Dans notre cas, chaque tache porte un identifiant unique (id
). Ajoutez donc la valeur de cet identifiant comme prop key
.
Correction
const App = () => { return <div> {/* ... */} {todoList.map(todo => <TodoItem id={todo.id} key={todo.id} // Notre nouvelle prop key name={todo.name} complete={todo.complete} />)} {/* ... */} </div>}
Une fois ceci terminé, faites un commit avec le commentaire "Ex 3 : Iterating on tasks" et poussez vers le serveur.
git add .git commit -m 'Ex 3 : Iterating on tasks'git push
#
Ajouter des tâchesNotre application commence à ressembler à quelque chose, mais il reste encore pas mal de travail.
Nous arrivons à afficher une liste de tâches depuis une variable, mais nous n'avons pas encore la possibilité d'ajouter une tâche.
#
Le composant FormOuvrez le fichier contenant le code du composant Form
que nous avons extrait tout Ă l'heure.
Ce composant contient le formulaire servant à ajouter une tâche à la liste.
#
Gestionnaire d'évènementsAussi appelés event handlers en anglais, les gestionnaires d'évènements sont des fonctions appelées lors d'un évènement. Vous en avez sûrement attaché à des éléments du DOM de la sorte :
const btn = document.querySelector('button')
const handleClick = () => alert('Clic !')
btn.addEventListener('click', handleClick)
Avec React, on attache ces fonctions directement sur l'élément :
SyntaxError: Unexpected token (1:8) 1 : return () ^
Dans l'exemple ci dessus, on attache notre gestionnaire d'évènement handleClick
sur l'évènement click
de l'élément button
en lui passant directement une prop onClick
note
En HTML, la syntaxe (découragée) des attributs event handlers utilise une syntaxe minuscule (ex: onclick), contrairement à React qui utilise la syntaxe camelCase
info
Par convention, on nomme généralement les gestionnaires d'évènements handle<Event>
(ex: handleClick
).
Ouvrez votre console Javascript après avoir cliqué sur ce bouton.
Comme vous pouvez le constater, l'argument passé à la fonction (ici evt
) est un objet contenant pas mal de propriétés. On notera principalement la propriété evt.target
, qui n'est autre que l'élément du DOM sur lequel l'action a eu lieu.
note
L'évènement passé à la fonction n'est pas le même que celui passé avec addEventListener
. C'est une surcouche créée par React pour simplifier son utilisation et gommer les différences entre les navigateurs.
Nous allons devoir ajouter un event handler pour être notifié de la modification du champs texte pour créer une nouvelle tâche. Mais d'abord, voyons un moyen de stocker cette valeur.
useState
#
Avez-vous essayé d'écrire du texte dans le champs ? C'est impossible car la prop value
passée à l'input
ne change pas.
Ce composant a besoin d'un moyen de conserver un état pour se souvenir de la valeur courante du champs texte.
Avec React, il existe une solution pour répondre à ce problème : le state
.
Pour créer un state
, on utilise la fonction useState
de React. Cette fonction spéciale est appellée un hook, nous en reparlerons plus tard.
Prenons par exemple ce composant Counter
:
SyntaxError: Unexpected token (1:8) 1 : return () ^
Comme vous pouvez le voir, le hook useState
retourne un Array
de deux éléments, que j'ai nommé count
et setCount
.
count
est une variable qui dont la valeur persiste entre les rendussetCount
est une fonction qui permet de modifiercount
Ici, j'appelle setCount
dans le gestionnaire d'évènement, en lui passant la valeur courante de count
plus un.
Par convention, on nomme les deux éléments retournés par
useState
sur le modèle<variable>
etset<Variable>
, comme plus haut :count
etsetCount
.
#
Champs contrôléMaintenant que nous savons créer un state
pour maintenir l'état de notre input
, on peut utiliser un event handler pour récupérer les changements dans 'input
et les mettre dans notre state
.
Essayez donc de le faire vous mĂŞme.
tip
Souvent, les inputs
utilisent un event handler nommé onChange
, et stockent leur valeur dans la propriété value de leur élément sur le DOM...
Correction
import React from 'react'
const Form = () => {
const [value, setValue] = useState('')
const handleChange = evt => { setValue(evt.target.value) }
return <form> {/* ... */} <input className="new-todo" placeholder="Qu'avez vous Ă faire ?" onChange={handleChange} value={value} autoFocus /> {/* ... */} </form>}
export default Form
Une fois ceci terminé, faites un commit avec le commentaire "Ex 4 : Controlled todo input" et poussez vers le serveur.
git add .git commit -m 'Ex 4 : Controlled todo input'git push
On nomme éléments contrôlés les éléments dont la valeur est asservie par React comme dans l'exemple ci-dessus. Cela permet d'avoir un plus grand contrôle sur la saisie de l'utilisateur, comme par exemple de forcer la première lettre à être une majuscule, ou limiter le nombre de caractères.
SyntaxError: Unexpected token (1:8) 1 : return () ^
#
FormulaireMaintenant que notre input
est éditable et que nous contrôlons sa valeur, il faudrait pouvoir ajouter une tâche dans la liste.
Comme vous avez pu le remarquer, l'input
est à l'intérieur d'un élément form
, qui contient aussi un bouton de soumission caché. Comme vous le savez, un tel formulaire est automatiquement soumis quand la touche entrée est frappée lorsque l'un des input
s enfants a le focus.
Essayez de taper quelque chose et d'appuyer sur entrée. Comme vous pouvez le constater, le formulaire est soumis : la page est rafraîchie. Il est possible d'empêcher ce rafraîchissement tout en détectant la soumission du formulaire à l'aide de l'event handler onSubmit
.
SyntaxError: Unexpected token (1:8) 1 : return () ^
Maintenant que nous savons la détecter, il nous faut notifier le composant parent (App
) de l'ajout d'une nouvelle tâche.
La "façon React" de le faire consiste à passer une fonction du composant App
vers le composant Form
de manière à ce que ce dernier puisse l'appeler avec la nouvelle tâche :
const App = () => { // ... const handleAdd = todoName => { alert('Nouveau todo : ' + todoName) }
return <> {/* ... */} <Form onAdd={handleAdd} /> {/* ... */} </>}
Comme vous pouvez le constater, le morceau de code précédent passe une prop nommée onAdd
au composant Form
, contenant la fonction handleAdd
.
tip
La convention veut qu'on nomme les fonctions passées en prop de la même manière que les event handlers, ce qui est un bon moyen pour rendre le code compréhensible.
Maintenant que nous passons une fonction Ă Form
, il nous suffit de l'appeler quand le formulaire est soumis !
Correction du composant Form
import React from 'react'
const Form = props => { const { onAdd } = props
const [value, setValue] = useState('')
const handleSubmit = evt => { evt.preventDefault() onAdd(value) }
return <form onSubmit={handleSubmit}> {/* ... */} </form>}
export default Form
Une fois ceci terminé, faites un commit avec le commentaire "Ex 5 : Submitting new todo form" et poussez vers le serveur.
git add .git commit -m 'Ex 5 : Submitting new todo form'git push
#
Modifier une listeLe composant App
utilise une variable qui contient la liste des tâches, mais si nous essayons de la modifier, elle sera remise à sa valeur d'origine à chaque affichage. Pour que le composant puisse s'en souvenir, il faut la mettre dans un state.
Une fois ceci fait, il nous suffit de modifier la fonction handleAdd
pour ajouter une tâche à la liste.
Ci-dessous, un petit exemple d'ajout d'élément dans une liste dans un state.
SyntaxError: Unexpected token (1:8) 1 : return () ^
info
Pourquoi ne pas avoir modifié directement le state
avec un push ? La réponse dans la petite partie théorique suivante.
#
ImmutabilitéDans le dernier exemple de code, il est suggéré d’utiliser le object spread operator pour créer une copie de la liste des tâches pour la modifier, plutôt que de modifier la liste existante.
Il y a généralement deux façons de modifier une donnée. La première approche est de muter la donnée en changeant directement sa valeur. La seconde approche est de remplacer la donnée par une copie qui a les changements désirés.
Changement avec mutation :
const ​player​ = {​score​: ​1​, ​name​: ​'Jeff'​}player​.​score​ = ​2​// Maintenant player est {score: 2, name: 'Jeff'}
Changement sans mutation :
const ​player​ = {​score​: ​1​, ​name​: ​'Jeff'​}const newPlayer = {...player, ​score​: ​2​}// Maintenant player est le même, mais newPlayer est {score: 2, name: 'Jeff'}
console.log(​player == newPlayer​) // Test d'égalité// trueconsole.log(​player === newPlayer​) // Test d'égalité stricte// false
#
Les fonctionnalités complexes deviennent simplesL'immutabilité rend des fonctionnalités complexes beaucoup plus simples à implémenter, comme par exemple les systèmes de retour en arrière.
Éviter de muter une donnée nous permet de conserver des versions de l'état courant, et ainsi de les réutiliser plus tard pour revenir en arrière.
#
Optimiser le renduLe plus gros benefice de l'immutabilité est que cela permet de créer des composants React dits purs. Une donnée immuable permet de déterminer facilement quand des changements ont été faits, ce qui permet de déterminer bien plus rapidement quand un composant doit mettre à jour son affichage en utilisant le test d'egalité stricte, qui est très performant.
Vous pouvez en apprendre davantage sur le sujet dans la documentation officielle de React.
#
Modifier la liste des tâchesEssayez à l'aide de ces précisions et de l'exemple précédent de le faire vous même pour le composant App
.
Correction
const App = () => {
const [todoList, setTodoList] = useState([{ // ... }])
const handleAdd = name => { setTodoList([ { id: 'todo-'+Date.now(), completed: false, name }, ...todoList ]) }
return <section className="todoapp"> <header className="header"> <h1>Todo</h1> <TodoForm onAdd={handleAdd} /> </header> {/* ... */} </section>}
Une fois ceci terminé, faites un commit avec le commentaire "Ex 6 : Adding todos" et poussez vers le serveur.
git add .git commit -m 'Ex 6 : Adding todos'git push
C'est la fin de cette première partie ! Félicitations d'être arrivés jusqu'ici. Il reste encore un peu de travail avant que notre application soit complète, mais nous en avons déjà fait une grosse partie.