d) L'état plus avancé

Comment gérer un formulaire en React ?

Introduction

Il y a beaucoup d'applications web où nous allons souhaiter gérer un formulaire.

En React, pour afficher ce qui est visible dans un formulaire, nous allons devoir jouer avec l'état de l'application.

Pour ce tutoriel, veuillez créer une copie du tutoriel start-state, si nécessaire voici le code du tutoriel start-state, et l'appeler medium-state. Changez le nom du projet dans package.json.

Il existe deux façons de gérer des formulaires en React :

  • les formulaires non contrôlés par React : les valeurs des inputs sont gérées par le DOM (Document Object Model) ; c'est la manière qui correspond à la manière de faire en JS pur, quand on fait de la programmation "old-school" ;
  • les formulaires contrôlés par React : les valeurs des inputs sont gérées par React, via un état.

Formulaire non contrôlé par React

Dans le composant Main (/src/components/Main/index.tsx), à la suite de PizzaMenu, nous allons ajouter un formulaire :

tsx
const Main = () => {
const handleSubmit = (e: SyntheticEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
console.log("submit:", form.pizza.value, form.description.value);
};
return (
<main>
<p>My HomePage</p>
<p>
Because we love JS, you can also click on the header to stop / start the
music ; )
</p>
<audio id="audioPlayer" controls autoPlay>
<source src={sound} type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<PizzaMenu />
<div>
<br />
<form onSubmit={handleSubmit}>
<label htmlFor="pizza">Pizza</label>
<input type="text" id="pizza" name="pizza" />
<label htmlFor="description">Description</label>
<input type="text" id="description" name="description" />
<button type="submit">Ajouter</button>
</form>
</div>

Ce code va être expliqué en découvrant ce qu'est l'objet "event" et comment l'utiliser.

L' objet "event"

L'objet event a été nommé e ci-dessus, mais nous aurions pu lui donner le nom que l'on souhaitait.

👍 Pour éviter la confusion, il est recommandé de l'appeler e (ou éventuellement event).

L'objet event est automatiquement passé à la callback d'un gestionnaire d'événements.

Il est très utile, pour deux raisons principalement :

  • Stopper l'action par défaut associée à un événement.
  • Lorsqu'on attache une même callback à une multitude d'éléments, pour retrouver la cible de l'événement.

e.target est l'élément HTML qui lance la propagation de l'événement dans l'arbre des composants.

Parfois on préfère utiliser e.currentTarget qui est l'élément HTML sur lequel est attaché le gestionnaire d'événements.

Dans le code ci-dessus, on utilise l'objet event pour stopper l'action par défaut d'un formulaire qui est d'envoyer les données au serveur (indiqué par la propriété action du formulaire) et de recharger la page.

Veuillez :

  • exécuter l'application et vérifiez que tout fonctionne ;
  • commenter e.preventDefault() :
ts
const handleSubmit = (e: SyntheticEvent) => {
// e.preventDefault();
const form = e.target as HTMLFormElement;
console.log("submit:", form.pizza.value, form.description.value);
};

Qu'est-ce qui se passe ?
Il y a un rechargement de page qui est interdit dans le type d'application que nous développons. Nous reviendrons plus tard sur pourquoi il n'est pas acceptable de recharger la page...

Formulaire contrôlé par React

Dans le formulaire développé jusque là, il n'est pas contrôlé par React. Nous avons accès à la valeur des inputs, néanmoins, ça n'est pas une pratique recommandée.

👍 Il est recommandé d'utiliser des composants contrôlés par React. Les valeurs des inputs doivent être contrôlées par React à travers un état, et les changements seront gérés via les gestionnaires d'événements (onChange pour les inputs).

Comme nous avons deux inputs ici, nous allons créer deux variables d'états et les mettre à jour dans la callback associée aux gestionnaires d'événements onChange :

tsx
1
const Main = () => {
2
const [pizza, setPizza] = useState("");
3
const [description, setDescription] = useState("");
4
5
const handleSubmit = (e: SyntheticEvent) => {
6
e.preventDefault();
7
console.log("submit:", pizza, description);
8
};
9
10
const handlePizzaChange = (e: SyntheticEvent) => {
11
const pizzaInput = e.target as HTMLInputElement;
12
console.log("change in pizzaInput:", pizzaInput.value);
13
setPizza(pizzaInput.value);
14
};
15
16
const handleDescriptionChange = (e: SyntheticEvent) => {
17
const descriptionInput = e.target as HTMLInputElement;
18
console.log("change in descriptionInput:", descriptionInput.value);
19
setDescription(descriptionInput.value);
20
};
21
22
return (
23
<main>
24
<p>My HomePage</p>
25
<p>
26
Because we love JS, you can also click on the header to stop / start the
27
music ; )
28
</p>
29
<audio id="audioPlayer" controls autoPlay>
30
<source src={sound} type="audio/mpeg" />
31
Your browser does not support the audio element.
32
</audio>
33
<PizzaMenu />
34
<div>
35
<br />
36
<form onSubmit={handleSubmit}>
37
<label htmlFor="pizza">Pizza</label>
38
<input
39
value={pizza}
40
type="text"
41
id="pizza"
42
name="pizza"
43
onChange={handlePizzaChange}
44
required
45
/>
46
<label htmlFor="description">Description</label>
47
<input
48
value={description}
49
type="text"
50
id="description"
51
name="description"
52
onChange={handleDescriptionChange}
53
required
54
/>
55
<button type="submit">Ajouter</button>
56
</form>
57
</div>

Nous voyons maintenant que :

  • chaque valeur d'une input est contrôlée par une variable d'état qui est mise à jour à chaque changement opéré (dans l'input) ;
  • que grâce à l'objet "event", nous avons accès à la valeur de chaque input via e.target.value. Néanmoins, comme TS est typé, nous devons d'abord faire une assertion de type pour e.target en indiquant que c'est un HTMLInputElement afin d'avoir accès à value.

Veuillez exécuter l'application, ouvrir la console, et observer ce qui se passe :

  • quand vous écrivez dans les inputs,
  • lorsque vous cliquez sur le bouton Ajouter.

Ca y est, nous avons appris comment maîtriser les formulaires en Flutter.

Il nous reste maintenant à voir comment utiliser les données du formulaire au sein d'une collection de données qui va permettre de mettre à jour nos écrans.

Gérer une collection comme état de l'application

Introduction

Très souvent, c'est une collection de données qui sera utilisée comme état de l'application.

Par exemple, dans notre tutoriel, nous souhaiterions qu'une collection de pizzas permette :

  • d'afficher toutes les pizzas du menu ;
  • d'ajouter automatiquement une nouvelle pizzas au menu après soumission des données du formulaire.

💭 Quand nous devons mettre en place une variable d'état, ici un array de Pizza, il faut toujours se poser la question : "Mais où est-ce que je dois gérer cet état ?".

Actuellement, la collection de Pizza est gérée dans le composant PizzaMenu, qui est un "sibling" (un frère ou une sœur) du formulaire. Ainsi, si nous devons y accéder dans ces différents éléments, il faut faire monter l'état vers leur ancêtre commun le plus proche, leur parent. Ici, c'est le composant Main.

Nous allons donc mettre à jour PizzaMenu pour qu'il reçoive dans ses props la collection de pizza. Voici PizzaMenu mis à jour :

tsx
1
import { Pizza } from "../../types";
2
import "./PizzaMenu.css";
3
4
interface PizzaMenuProps {
5
pizzas: Pizza[];
6
}
7
8
const PizzaMenu = ({ pizzas }: PizzaMenuProps) => {
9
return (
10
<table className="pizza-menu">
11
<thead>
12
<tr>
13
<th>Pizza</th>
14
<th>Description</th>
15
</tr>
16
</thead>
17
<tbody>
18
{pizzas.map((pizza) => (
19
<tr key={pizza.id}>
20
<td>{pizza.title}</td>
21
<td>{pizza.content}</td>
22
</tr>
23
))}
24
</tbody>
25
</table>
26
);
27
};
28
29
export default PizzaMenu;

Nous avons créé l'interface Pizza dans un nouveau fichier /src/types.ts :

ts
interface Pizza {
id: number;
title: string;
content: string;
}
export type { Pizza };

Pour simplifier le développement, nous vous conseillons d'enlever l'autoPlay pour l'audio... Ca fera moins de bruit ; )

Voici la mise à jour du composant Main afin de passer une variable d'état initialisée par les pizzas par défaut du menu (et enlever l'autoPlay de l'audio):

tsx
1
const defaultPizzas = [
2
{
3
id: 1,
4
title: "4 fromages",
5
content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",
6
},
7
{
8
id: 2,
9
title: "Vegan",
10
content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",
11
},
12
{
13
id: 3,
14
title: "Vegetarian",
15
content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",
16
},
17
{
18
id: 4,
19
title: "Alpage",
20
content: "Gruyère, Mozarella, Lardons, Tomates",
21
},
22
{
23
id: 5,
24
title: "Diable",
25
content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",
26
},
27
];
28
29
const Main = () => {
30
const [pizza, setPizza] = useState("");
31
const [description, setDescription] = useState("");
32
const [pizzas] = useState(defaultPizzas);
33
34
const handleSubmit = (e: SyntheticEvent) => {
35
e.preventDefault();
36
console.log("submit:", pizza, description);
37
};
38
39
const handlePizzaChange = (e: SyntheticEvent) => {
40
const pizzaInput = e.target as HTMLInputElement;
41
console.log("change in pizzaInput:", pizzaInput.value);
42
setPizza(pizzaInput.value);
43
};
44
45
const handleDescriptionChange = (e: SyntheticEvent) => {
46
const descriptionInput = e.target as HTMLInputElement;
47
console.log("change in descriptionInput:", descriptionInput.value);
48
setDescription(descriptionInput.value);
49
};
50
51
return (
52
<main>
53
<p>My HomePage</p>
54
<p>
55
Because we love JS, you can also click on the header to stop / start the
56
music ; )
57
</p>
58
<audio id="audioPlayer" controls >
59
<source src={sound} type="audio/mpeg" />
60
Your browser does not support the audio element.
61
</audio>
62
<PizzaMenu pizzas={pizzas} />
63
// Reste du code

L'application s'affiche comme auparavant.

Nous allons maintenant faire en sorte que lors du submit, on mette à jour la variable d'état pizzas.

Mise à jour de l'état en React

En React, nous ne devons jamais mettre à jour l'état directement. Lors du submit, nous pourrions être tenté de faire quelque chose du genre :

ts
const newPizza = {
id: nextPizzaId(pizzas),
title: pizza,
content: description,
};
pizzas.push(newPizza);
setPizzas(pizzas);

👎 Si vous faites cela, ça pourrait marcher, et vous pourriez vous en sortir malgré tout.
Néanmoins, vous risquez d'avoir des soucis de debugging (vous ne pouvez pas suivre les changements d'états), d'optimisation, et même de comportement erratique...

👍 Retenez qu'en React, l'état est immuable. Si vous souhaitez le changer, vous devez chaque fois passer un nouvel objet à votre fonction mettant à jour l'état.

Par exemple, pour mettre à jour un array, vous avez deux options. Soit vous utilisez la fonction concat qui crée un nouvel array, ajoute l'élément, et renvoie le nouvel array :

ts
setPizzas(pizzas.concat(newPizza));

Soit vous utilisez le spread operator pour créer un nouvel array contenant tous les éléments de pizzas, en y ajoutant le dernier élément :

ts
setPizzas([...pizzas, newPizza]);

Voici le code final du Main dans lequel nous avons ajouté une fonction toute à la fin permettant de générer un identifiant :

tsx
1
import { SyntheticEvent, useState } from "react";
2
import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";
3
import DrinkCard from "./DrinkCard";
4
import DrinkMenu from "./DrinkMenu";
5
import "./Main.css";
6
import PizzaMenu from "./PizzaMenu";
7
import { Pizza } from "../../types";
8
9
10
const defaultPizzas = [
11
{
12
id: 1,
13
title: "4 fromages",
14
content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",
15
},
16
{
17
id: 2,
18
title: "Vegan",
19
content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",
20
},
21
{
22
id: 3,
23
title: "Vegetarian",
24
content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",
25
},
26
{
27
id: 4,
28
title: "Alpage",
29
content: "Gruyère, Mozarella, Lardons, Tomates",
30
},
31
{
32
id: 5,
33
title: "Diable",
34
content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",
35
},
36
] ;
37
38
const Main = () => {
39
const [pizza, setPizza] = useState("");
40
const [description, setDescription] = useState("");
41
const [pizzas, setPizzas] = useState(defaultPizzas);
42
43
const handleSubmit = (e: SyntheticEvent) => {
44
e.preventDefault();
45
console.log("submit:", pizza, description);
46
const newPizza = {
47
id: nextPizzaId(pizzas),
48
title: pizza,
49
content: description,
50
};
51
52
setPizzas([...pizzas, newPizza]);
53
};
54
55
const handlePizzaChange = (e: SyntheticEvent) => {
56
const pizzaInput = e.target as HTMLInputElement;
57
console.log("change in pizzaInput:", pizzaInput.value);
58
setPizza(pizzaInput.value);
59
};
60
61
const handleDescriptionChange = (e: SyntheticEvent) => {
62
const descriptionInput = e.target as HTMLInputElement;
63
console.log("change in descriptionInput:", descriptionInput.value);
64
setDescription(descriptionInput.value);
65
};
66
67
return (
68
<main>
69
<p>My HomePage</p>
70
<p>
71
Because we love JS, you can also click on the header to stop / start the
72
music ; )
73
</p>
74
<audio id="audioPlayer" controls >
75
<source src={sound} type="audio/mpeg" />
76
Your browser does not support the audio element.
77
</audio>
78
<PizzaMenu pizzas={pizzas} />
79
80
<div>
81
<br />
82
<form onSubmit={handleSubmit}>
83
<label htmlFor="pizza">Pizza</label>
84
<input
85
value={pizza}
86
type="text"
87
id="pizza"
88
name="pizza"
89
onChange={handlePizzaChange}
90
required
91
/>
92
<label htmlFor="description">Description</label>
93
<input
94
value={description}
95
type="text"
96
id="description"
97
name="description"
98
onChange={handleDescriptionChange}
99
required
100
/>
101
<button type="submit">Ajouter</button>
102
</form>
103
</div>
104
105
<DrinkMenu title="Notre Menu de Boissons">
106
<DrinkCard
107
title="Coca-Cola"
108
image="https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8="
109
>
110
<p>Volume: 33cl</p>
111
<p>Prix: 2,50 €</p>
112
</DrinkCard>
113
<DrinkCard
114
title="Pepsi"
115
image="https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg="
116
>
117
<p>Volume: 33cl</p>
118
<p>Prix: 2,50 €</p>
119
</DrinkCard>
120
<DrinkCard
121
title="Eau Minérale"
122
image="https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk="
123
>
124
<p>Volume: 50cl</p>
125
<p>Prix: 1,50 €</p>
126
</DrinkCard>
127
</DrinkMenu>
128
</main>
129
);
130
};
131
132
const nextPizzaId = (pizzas: Pizza[]) => {
133
return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;
134
};
135
136
export default Main;

Notons que la fonction reduce est très intéressante :

  • c'est de la programmation fonctionnelle, reduce reçoit une fonction en paramètre (on appelle ça une callback)
  • elle permet d'itérer sur tous les éléments d'une collection, en appelant la callback sur chaque élément de la collection ; chaque élément est reçu dans le deuxième argument de la callback appelé pizza ici ;
  • à chaque appel de la callback, le résultat de l'itération précédente est récupéré dans le 1er argument de la callback appelé maxId ici ;
  • à la 1ère itération, on considère la valeur 0 comme valeur précédente ; c'est d'ailleurs la valeur 0 qui serait renvoyée s'il n'y a pas d'éléments dans la collection.

Exercice 2.7 : Gestion d'une collection pour l'état

Veuillez créer un nouveau projet en utilisant les technos Vite + React + TS + SWC nommé /exercises/2.7 dans votre git repo.

Veuillez créer une nouvelle application qui vous permette, dans la même page :

  • d'afficher 5 de vos films préférés.
  • d'ajouter un film via un formulaire.

Un film devra avoir :

  • un titre
  • un director
  • une durée en minutes

Un film pourra avoir :

  • un lien vers une image
  • une description
  • un budget (en million)

Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.7

Gestion d'un état partagé par plusieurs composants

Quand un état est partagé par plusieurs composants, sa gestion peut se compliquer.

Pour notre tutoriel, afin de bien structurer notre code, nous allons créer un nouveau composant AddPizza dans /src/components/Main/AddPizza.tsx :

tsx
import { useState, SyntheticEvent } from "react";
import { NewPizza } from "../../types";
interface AddPizzaProps {
addPizza: (pizza: NewPizza) => void;
}
const AddPizza = ({ addPizza }: AddPizzaProps) => {
const [pizza, setPizza] = useState("");
const [description, setDescription] = useState("");
const handleSubmit = (e: SyntheticEvent) => {
e.preventDefault();
console.log("submitting pizza:", pizza, description);
addPizza({ title: pizza, content: description });
};
const handlePizzaChange = (e: SyntheticEvent) => {
const pizzaInput = e.target as HTMLInputElement;
console.log("change in pizzaInput:", pizzaInput.value);
setPizza(pizzaInput.value);
};
const handleDescriptionChange = (e: SyntheticEvent) => {
const descriptionInput = e.target as HTMLInputElement;
console.log("change in descriptionInput:", descriptionInput.value);
setDescription(descriptionInput.value);
};
return (
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="pizza">Pizza</label>
<input
value={pizza}
type="text"
id="pizza"
name="pizza"
onChange={handlePizzaChange}
required
/>
<label htmlFor="description">Description</label>
<input
value={description}
type="text"
id="description"
name="description"
onChange={handleDescriptionChange}
required
/>
<button type="submit">Ajouter</button>
</form>
</div>
);
};
export default AddPizza;

Nous avons créé un nouveau type NewPizza qui est quasi identique au type Pizza, à la différence qu'il ne contient pas d'id :

ts
1
interface Pizza {
2
id: number;
3
title: string;
4
content: string;
5
}
6
7
type NewPizza = Omit<Pizza, "id">;
8
9
export type { Pizza, NewPizza };

Le composant AddPizza reçoit de son parent la callback qui permet de mettre à jour l'état géré par le parent !

💭 Nous pouvons maintenant bien assimiler comment un composant "enfant" peut renvoyer de l'information à son parent. C'est via la callback que l'enfant reçoit, lorsqu'il l'appelle, qu'il passera en paramètre ses données. Ici, l'enfant passe comme info au parent une nouvelle pizza : addPizza({ title: pizza, content: description });

Il est à noter que comme l'enfant n'a pas accès à toutes les pizzas et leur identifiants, c'est le parent qui devra générer un identifiant.

Ainsi, le composant Main est simplifié en faisant appel à AddPizza :

tsx
1
import { useState } from "react";
2
import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";
3
import DrinkCard from "./DrinkCard";
4
import DrinkMenu from "./DrinkMenu";
5
import "./Main.css";
6
import PizzaMenu from "./PizzaMenu";
7
import { NewPizza, Pizza } from "../../types";
8
import AddPizza from "./AddPizza";
9
10
11
const defaultPizzas = [
12
{
13
id: 1,
14
title: "4 fromages",
15
content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",
16
},
17
{
18
id: 2,
19
title: "Vegan",
20
content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",
21
},
22
{
23
id: 3,
24
title: "Vegetarian",
25
content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",
26
},
27
{
28
id: 4,
29
title: "Alpage",
30
content: "Gruyère, Mozarella, Lardons, Tomates",
31
},
32
{
33
id: 5,
34
title: "Diable",
35
content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",
36
},
37
] ;
38
39
const Main = () => {
40
41
const [pizzas, setPizzas] = useState(defaultPizzas);
42
43
44
const addPizza = (newPizza:NewPizza) => {
45
const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };
46
setPizzas([...pizzas, pizzaAdded]);
47
};
48
49
50
return (
51
<main>
52
<p>My HomePage</p>
53
<p>
54
Because we love JS, you can also click on the header to stop / start the
55
music ; )
56
</p>
57
<audio id="audioPlayer" controls >
58
<source src={sound} type="audio/mpeg" />
59
Your browser does not support the audio element.
60
</audio>
61
<PizzaMenu pizzas={pizzas} />
62
63
<div>
64
<br />
65
<AddPizza addPizza={addPizza} />
66
</div>
67
68
<DrinkMenu title="Notre Menu de Boissons">
69
<DrinkCard
70
title="Coca-Cola"
71
image="https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8="
72
>
73
<p>Volume: 33cl</p>
74
<p>Prix: 2,50 €</p>
75
</DrinkCard>
76
<DrinkCard
77
title="Pepsi"
78
image="https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg="
79
>
80
<p>Volume: 33cl</p>
81
<p>Prix: 2,50 €</p>
82
</DrinkCard>
83
<DrinkCard
84
title="Eau Minérale"
85
image="https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk="
86
>
87
<p>Volume: 50cl</p>
88
<p>Prix: 1,50 €</p>
89
</DrinkCard>
90
</DrinkMenu>
91
</main>
92
);
93
};
94
95
const nextPizzaId = (pizzas: Pizza[]) => {
96
return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;
97
};
98
99
export default Main;

💭 Vous devriez à présent avoir les réponses à ces questions :

  • Comment passer de l'info d'un parent vers ses enfants ?
  • Comment passer de l'info d'un enfant vers un ancêtre ?
  • Comment passer de l'info d'un sibling (frère ou sœur) vers un autre sibling ?

Si nécessaire, vous pouvez trouver le code associé à ce tutoriel ici : medium-state.

Debugging d'une application React

Introduction

💭 Qui est votre meilleur ami ?

Il est possible qu'à ce stade-ci, vous ignorez une des bonnes réponses, car pour les développeurs, le debugger est leur meilleur ami !

Le debugger est toujours là pour vous, prêt à vous faire voyager pas à pas dans votre code, à vous donner des pistes dans les moments difficiles, sans imposer de solutions, il vous offre une liberté totale ! Et il acceptera toujours votre code tel qu'il est, sous réserve bien sûr que celui-ci compile. C'est exactement ce que l'on attend d'un ami 😁.

Debugging sous Chrome

Pour accéder aux outils de debugging sous Chrome :

  • Soit clic droit sur votre page Web, Inspect.
  • Soit F12.
  • Clic sur le tab Sources.
  • Dans localhost:5173 (ou un autre port en fonction du port associé à votre app, 5173 est le port par défaut d'une application exécutée par Vite), dans src, vous trouverez vos fichiers .tsx. Il y a chaque fois deux versions d'un même fichier ; celle qui vous intéresse est celle dont le nom de fichier est écrit en italique : c'est un map de vos sources contenant le code TSX / TS.

Pour déboguer un fichier .tsx ou .ts :

  • Ajout de breakpoint : clic sur le numéro de la ligne à gauche du code.

  • Utilisation des flèches pour exécuter et naviguer dans le code :

    • Step over : pour aller à la ligne de code suivante.
    • Step into : pour entrer dans une fonction.
    • Resume : pour aller au prochain breakpoint, est souvent utilisé quand on veut avancer plus vite dans du code contenant de nombreuses lignes.

    Debugging dans VS code

    Veuillez ouvrir votre application dans VS Code en tant que Workspace. Pour ce faire : File, Open Folder... et sélectionnez le répertoire de votre application Vite + React + TS.

    La toute première fois, vous devez créer une configuration pour votre debugger :

    • Cliquez sur `Run and Debug.
    • Cliquez sur create a launch.json file.
    • Sélectionnez Web App (Chrome).

    Vous obtenez une configuration dans .vscode/launch.json. Veuillez changer le port de l'url vers le port par défaut de Vite (5173):

json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}"
}
]
}

D'abord lancez votre application à l'aide de la commande habituelle : npm run dev.

Maintenant que votre application tourne, vous allez lui attacher le debugger. Il suffit de cliquer sur F5 pour lancer le debugger.

🍬 Challenge optionnel 2.8 : gestion d'un état partagé

Si vous le souhaitez, et que vous vous sentez avancé dans ce cours, vous pourriez découvrir des concepts par vous-même à travers ce challenge. Vous pouvez créer un nouveau projet en copiant le code du tutoriel nommé medium-state en tant que répertoire /exercises/2.8 dans votre git repo.

Vous avez remarqué que dans le composant Main, il est écrit : "Because we love JS, you can also click on the header to stop / start the music ; )"

Vous pourriez choisir cette mission : à l'aide de JS/TS, veuillez faire en sorte que l'on puisse cliquer sur le Header et que cela démarre ou stop la musique de l'élément <audio> présent dans le main.

🤝 Tips

  • Utilisation du Hook useRef pour obtenir une référence directe et persistante à l'élément <audio>, qui peut être mutée, ce qui permet d'interagir avec cet élément DOM de manière impérative, par exemple pour appeler la méthode play ou pause. L'avantage de cette méthode c'est qu'elle ne provoque pas de re-render du composant quand il est mis à jour (à l'inverse de si l'on faisait de la programmation old-school avec document.getElementById pour récupérer une référence à <audio>).
  • Utilisation du Hook useEffect pour réaliser une action à chaque fois que l'on a une action à réaliser parce qu'il y aurait eu un clic dans le Header. Dans ce cas-ci, il faudrait bien comprendre le useEffect pour l'associer au changement d'une variable d'état (qui représente s'il y a besoin d'une action à faire à cause d'un clic dans le header).

Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.8+.