Skip to main content

TP1 - Todolist - Partie 1

Intro#

Avant de commencer#

Lors 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é-requis#

Il 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 Git#

Dans 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éveloppement#

Dans 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épart#

Vous 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 composants#

Le 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".

Le composant TodoItem#

Extrayez cet élément dans un nouveau fichier components/TodoItem.jsx.

Correction
src/components/TodoItem.jsx
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

Le composant Form#

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
src/components/Form.jsx
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

Passer des props#

Maintenant que nous disposons d'un composant TodoItem séparé, utilisons le dans le code de notre composant App :

<ul>  <TodoItem />  <TodoItem />  <TodoItem /></ul>

Passer name#

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

Passer complete#

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.

Passer id#

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ération#

Nous 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
src/components/App.jsx
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âches#

Notre 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 Form#

Ouvrez 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ènements#

Aussi 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 :

Live Editor
Result
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 :

Live Editor
Result
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 rendus
  • setCount est une fonction qui permet de modifier count

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> et set<Variable>, comme plus haut : count et setCount.

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
src/components/Form.jsx
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.

Live Editor
Result
SyntaxError: Unexpected token (1:8)
1 : return ()
            ^

Formulaire#

Maintenant 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 inputs 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.

Live Editor
Result
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
src/components/Form.jsx
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 liste#

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

Live Editor
Result
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 simples#

L'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 rendu#

Le 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âches#

Essayez à l'aide de ces précisions et de l'exemple précédent de le faire vous même pour le composant App.

Correction
src/components/App.jsx
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.