Cours JavaScript et React · Module 12: React et TypeScript
Typer useState et useRef
Dans cette leçon5
La liste des planètes démarre vide, la planète sélectionnée n'existe pas au départ, et le champ de recherche doit recevoir le focus après un clic. Sans types, chacune de ces situations se termine par la question "qu'y a-t-il vraiment ici ?". Les hooks React sont les moteurs de ton vaisseau spatial. TypeScript te permet de définir quelles données circulent dans ces moteurs, pour une navigation sûre.
Typer useState
Le hook useState déduit automatiquement le type à partir de la valeur initiale, mais parfois tu as besoin d'un typage explicite. Tu passes le type explicite entre chevrons juste après le nom du hook, par exemple useState<string> - c'est ce qu'on appelle un paramètre générique. Commençons par les cas où tu n'as rien à écrire :
1import { useState } from 'react';
2
3// Inférence automatique du type
4const [count, setCount] = useState(0); // number
5const [name, setName] = useState("Apollo"); // string
6const [active, setActive] = useState(true); // booleanTypeScript regarde 0, "Apollo" et true et fixe définitivement le type de l'état. L'appel setCount("beaucoup") est refusé, alors que nous n'avons jamais écrit le mot number.
C'est différent quand la valeur initiale ne révèle pas le type visé. La planète sélectionnée démarre à null, et à partir de null seul, le compilateur ne peut pas deviner qu'un objet Planet apparaîtra plus tard. On passe donc le type union Planet | null :
1// Typage explicite - nécessaire quand la valeur initiale est null ou undefined
2interface Planet {
3 id: number;
4 name: string;
5 type: string;
6}
7
8// L'état peut être Planet ou null
9const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
10
11// Tableau d'objets - un tableau vide n'indique pas à TypeScript le type des éléments
12const [planets, setPlanets] = useState<Planet[]>([]);
13
14// Types union
15type Status = "idle" | "loading" | "success" | "error";
16const [status, setStatus] = useState<Status>("idle");Ce qui n'a PAS changé : dans l'application en marche, useState se comporte exactement de la même façon. Le générique indique seulement au compilateur quelles valeurs peuvent être passées au setter. Grâce à Status, la faute setStatus("loadnig") ne passera pas.
Quand le typage explicite est-il nécessaire ?
Voici un aide-mémoire de quatre situations où il vaut la peine de donner un type. Il utilise une interface User - une interface d'utilisateur ordinaire, construite comme Planet. Nouvelle aussi, l'interface FormData avec un champ optionnel, que tu connais depuis la leçon précédente.
1// 1. La valeur initiale est null/undefined
2const [user, setUser] = useState<User | null>(null);
3
4// 2. Tableau vide
5const [items, setItems] = useState<string[]>([]);
6
7// 3. Types union avec des valeurs restreintes
8const [theme, setTheme] = useState<"dark" | "light">("dark");
9
10// 4. Objet complexe avec des champs optionnels
11interface FormData {
12 name: string;
13 email: string;
14 message?: string;
15}
16const [form, setForm] = useState<FormData>({ name: "", email: "" });Dans le cas 3, sans générique, TypeScript considérerait theme comme n'importe quel string. Dans le cas 4, le générique permet d'omettre message au départ et de l'ajouter plus tard. Conseil pratique : quand la valeur initiale montre clairement le type, n'écris pas le générique - moins de bruit, code plus lisible.
Typer useRef
Le hook useRef sert à stocker des références vers des éléments du DOM ou des valeurs qui ne provoquent pas de nouveau rendu. Pour les éléments du DOM, tu passes le type d'élément du navigateur, par exemple HTMLInputElement pour un champ texte, et null comme valeur initiale, car l'élément n'existe pas encore lors du premier rendu :
1import { useRef } from 'react';
2
3// Référence à un élément du DOM
4const inputRef = useRef<HTMLInputElement>(null);
5const divRef = useRef<HTMLDivElement>(null);
6const canvasRef = useRef<HTMLCanvasElement>(null);Dans React 19, l'argument de useRef est obligatoire - useRef<HTMLInputElement>() seul provoque une erreur de type. Voici une référence dans un composant. L'opérateur ?. (chaînage optionnel) n'appelle la méthode que si la valeur à sa gauche n'est ni null ni undefined :
1function SearchBar() {
2 const inputRef = useRef<HTMLInputElement>(null);
3
4 const focusInput = () => {
5 // inputRef.current peut être null, on utilise donc ?.
6 inputRef.current?.focus();
7 };
8
9 return (
10 <div>
11 <input ref={inputRef} placeholder="Search for a planet..." />
12 <button onClick={focusInput}>Focus</button>
13 </div>
14 );
15}React écrit l'élément dans inputRef.current après le montage du composant, c'est pourquoi current a le type HTMLInputElement | null. L'attribut ref en JSX accepte cette référence sans conversion supplémentaire.
useRef comme "conteneur" de valeur
useRef peut aussi contenir n'importe quelle valeur qui ne déclenche pas de rendu - un identifiant de minuteur, une valeur précédente, un compteur d'appels. Modifier current ne rafraîchit pas l'écran, contrairement au setter de useState.
1// ID du minuteur
2const timerRef = useRef<number | null>(null);
3
4function Countdown() {
5 const timerRef = useRef<number | null>(null);
6 const [seconds, setSeconds] = useState(10);
7
8 const start = () => {
9 timerRef.current = window.setInterval(() => {
10 setSeconds(prev => prev - 1);
11 }, 1000);
12 };
13
14 const stop = () => {
15 if (timerRef.current !== null) {
16 clearInterval(timerRef.current);
17 timerRef.current = null;
18 }
19 };
20
21 return (
22 <div>
23 <p>Countdown: {seconds}s</p>
24 <button onClick={start}>Start</button>
25 <button onClick={stop}>Stop</button>
26 </div>
27 );
28}
29
30// Valeur précédente
31const prevValueRef = useRef<string>("");Pourquoi window.setInterval et pas simplement setInterval ? Dans le navigateur, la fonction renvoie un nombre, mais quand les types Node.js sont aussi présents dans le projet, setInterval seul peut être typé comme un objet Timeout. Le préfixe window. lève le doute et correspond à number | null. Dans React 19, toutes les références sont modifiables, donc l'affectation à timerRef.current est parfaitement légale.
Éléments HTML courants dans useRef
Pour finir, un aide-mémoire des types d'éléments qui se retrouvent le plus souvent dans des références. Le nom commence toujours par HTML et se termine par Element :
1// Types d'éléments les plus couramment utilisés
2const inputRef = useRef<HTMLInputElement>(null);
3const textareaRef = useRef<HTMLTextAreaElement>(null);
4const selectRef = useRef<HTMLSelectElement>(null);
5const buttonRef = useRef<HTMLButtonElement>(null);
6const formRef = useRef<HTMLFormElement>(null);
7const divRef = useRef<HTMLDivElement>(null);
8const imgRef = useRef<HTMLImageElement>(null);
9const videoRef = useRef<HTMLVideoElement>(null);Inutile de les mémoriser - l'éditeur propose le nom dès que tu tapes HTML. Tu retrouveras ces mêmes types d'éléments dans la prochaine leçon, comme paramètre des types d'événements.
Code de cette leçon : App.tsx
1import React, { useState, useRef } from 'react';
2
3// Interface d'une planète
4interface Planet {
5 id: number;
6 name: string;
7 type: string;
8 distanceLy: number;
9}
10
11function App() {
12 // useState avec inférence automatique
13 const [count, setCount] = useState(0);
14
15 // useState avec type explicite - valeur initiale null
16 const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
17
18 // useState avec un tableau d'objets
19 const [planets, setPlanets] = useState<Planet[]>([
20 { id: 1, name: "Mars", type: "rocheuse", distanceLy: 0.000024 },
21 { id: 2, name: "Jupiter", type: "gazeuse", distanceLy: 0.000082 },
22 { id: 3, name: "Kepler-442b", type: "rocheuse", distanceLy: 1206 },
23 ]);
24
25 // useState avec un type union
26 const [status, setStatus] = useState<"idle" | "scanning" | "complete">("idle");
27
28 // useRef vers un élément du DOM
29 const searchRef = useRef<HTMLInputElement>(null);
30
31 // useRef comme conteneur de valeur (minuteur)
32 const scanTimerRef = useRef<number | null>(null);
33
34 const focusSearch = () => {
35 searchRef.current?.focus();
36 };
37
38 const startScan = () => {
39 setStatus("scanning");
40 scanTimerRef.current = window.setTimeout(() => {
41 setStatus("complete");
42 setCount(prev => prev + 1);
43 }, 2000);
44 };
45
46 const cancelScan = () => {
47 if (scanTimerRef.current !== null) {
48 clearTimeout(scanTimerRef.current);
49 scanTimerRef.current = null;
50 setStatus("idle");
51 }
52 };
53
54 const statusColors = {
55 idle: "#9ca3af",
56 scanning: "#f59e0b",
57 complete: "#22c55e"
58 };
59
60 return (
61 <div className="app">
62 <h1>Scanner cosmique</h1>
63
64 <div className="status-bar">
65 <span style={{ color: statusColors[status] }}>
66 Statut : {status.toUpperCase()}
67 </span>
68 <span>Scans terminés : {count}</span>
69 </div>
70
71 <div className="controls">
72 <input ref={searchRef} placeholder="Rechercher une planète..." className="search" />
73 <button onClick={focusSearch}>Focus</button>
74 <button onClick={startScan} disabled={status === "scanning"}>
75 {status === "scanning" ? "Analyse en cours..." : "Scanner"}
76 </button>
77 {status === "scanning" && (
78 <button onClick={cancelScan} className="cancel">Annuler</button>
79 )}
80 </div>
81
82 <div className="planets">
83 {planets.map(planet => (
84 <div
85 key={planet.id}
86 className={"planet-card " + (selectedPlanet?.id === planet.id ? "selected" : "")}
87 onClick={() => setSelectedPlanet(planet)}
88 >
89 <h3>{planet.name}</h3>
90 <p>Type : {planet.type}</p>
91 <p>Distance : {planet.distanceLy} ly</p>
92 </div>
93 ))}
94 </div>
95
96 {selectedPlanet && (
97 <div className="detail">
98 <h2>Sélectionnée : {selectedPlanet.name}</h2>
99 <p>Type : {selectedPlanet.type}</p>
100 <p>Distance : {selectedPlanet.distanceLy} années-lumière</p>
101 <button onClick={() => setSelectedPlanet(null)}>Fermer</button>
102 </div>
103 )}
104 </div>
105 );
106}
107
108export default App;Retiens : useState alimente ce que l'équipage voit à l'écran, tandis que useRef travaille en silence dans la salle des machines - les deux méritent un type précis.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quand est-il nécessaire de spécifier explicitement le type générique dans useState ?
2. Comment type-t-on correctement useRef pour stocker une référence à un élément input ?
Tâches pratiques dans le jeu
- Éditeur de code
Typer useState avec différents types
- Clique dans l'ordre
Ordonne la syntaxe pour étendre une interface en TypeScript :
- Éditeur de code
Typer useRef pour des éléments du DOM et des valeurs
- Tri vertical
Ordonne les étapes de création d'un état typé en React dans le bon ordre :
- Tri horizontal
Ordonne la syntaxe de useState avec un type générique explicite :