Cours Vibe coding avec l'IA · Module 3: Routing et composants

useRef - Références et composants non contrôlés

13 min de lecture
Dans cette leçon10

Tu as appris useState pour la gestion de l'état. Tu vas maintenant apprendre useRef - un hook pour créer des références vers des éléments du DOM et stocker des valeurs qui ne provoquent pas de re-rendu.

Qu'est-ce que useRef ?

useRef est un hook qui retourne un objet ref avec une propriété .current. Cet objet survit entre les re-rendus mais modifier .current ne provoque pas de re-rendu.

Import

Tu importes le hook depuis le paquet react, comme useState :

1import { useRef } from 'react';

Utilisation de base

Tu passes la valeur initiale entre parenthèses, puis tu lis et modifies la valeur actuelle via la propriété .current :

1const myRef = useRef(initialValue);
2
3// Lire la valeur
4console.log(myRef.current);
5
6// Modifier la valeur
7myRef.current = newValue;

useRef vs useState

CaractéristiqueuseStateuseRef
Re-renduProvoque un re-renduNe provoque pas de re-rendu
PersistanceEntre les rendusEntre les rendus
UsageDonnées de l'UIRéférences DOM, valeurs auxiliaires
Syntaxe[value, setValue].current

Exemple de la différence

Ce composant a deux compteurs : l'un dans l'état, l'autre dans une ref. Clique sur les deux boutons et compare ce qui se passe à l'écran et dans la console :

1function ClickTracker() {
2  const [count, setCount] = useState(0);  // useState
3  const clicksRef = useRef(0);            // useRef
4
5  return (
6    <div>
7      <p>Compteur de l'état : {count}</p>
8      <button onClick={() => setCount(count + 1)}>+1 dans l'état</button>
9      <button
10        onClick={() => {
11          clicksRef.current = clicksRef.current + 1;
12          console.log('Clics enregistrés dans la ref :', clicksRef.current);
13        }}
14      >
15        +1 dans la ref
16      </button>
17    </div>
18  );
19}

Ce qui se passe :

  1. Tu cliques sur « +1 dans l'état » → setCount modifie l'état → le composant refait son rendu et affiche le nouveau nombre
  2. Tu cliques sur « +1 dans la ref » → clicksRef.current augmente (tu le vois dans la console), mais l'écran ne change pas, car modifier une ref ne déclenche pas de rendu

Résultat : deux clics sur « +1 dans la ref » affichent 1 et 2 dans la console, tandis que l'écran reste sur « Compteur de l'état : 0 ». On modifie .current dans le gestionnaire de clic, et non dans le corps du composant, car la documentation de React interdit de lire et d'écrire ref.current pendant le rendu (sauf pour l'initialisation).

useRef pour les éléments du DOM

L'usage le plus courant de useRef - un accès direct aux éléments du DOM.

Exemple 1 : Focus sur un input

Le champ de recherche reçoit une ref, et le bouton y place le curseur avec la méthode focus() intégrée au navigateur :

1function SearchForm() {
2  const inputRef = useRef<HTMLInputElement>(null);
3
4  const handleFocus = () => {
5    // Appel direct d'une méthode du DOM
6    inputRef.current?.focus();
7  };
8
9  return (
10    <div>
11      <input
12        ref={inputRef}
13        type="text"
14      />
15      <button onClick={handleFocus}>
16        Mettre le focus sur l'input
17      </button>
18    </div>
19  );
20}

Explication :

  • inputRef.current est une référence vers l'élément <input>
  • Tu peux appeler des méthodes du DOM comme .focus(), .blur(), .scrollIntoView()

Exemple 2 : Défiler jusqu'à un élément

Un long fil a besoin d'un bouton pour remonter en haut. La ref pointe vers le titre, et scrollIntoView y fait défiler la page en douceur :

1function PirateFeed() {
2  const topRef = useRef<HTMLDivElement>(null);
3
4  const scrollToTop = () => {
5    topRef.current?.scrollIntoView({ behavior: 'smooth' });
6  };
7
8  return (
9    <div>
10      <div ref={topRef} className="text-2xl font-bold mb-4">
11        Fil de Kraken's Call
12      </div>
13
14      {/* Longue liste de posts... */}
15      <div className="space-y-4">
16        {posts.map(post => <PostCard key={post.id} post={post} />)}
17      </div>
18
19      {/* Bouton pour remonter en haut */}
20      <button
21        onClick={scrollToTop}
22        className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-full"
23      >
24        ↑
25      </button>
26    </div>
27  );
28}

Exemple 3 : Mesurer les dimensions d'un élément

Une ref permet aussi de mesurer un élément après son rendu. L'effet lit les dimensions de la galerie et les enregistre dans l'état pour les afficher à l'écran :

1function ImageGallery() {
2  const containerRef = useRef<HTMLDivElement>(null);
3  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
4
5  useEffect(() => {
6    if (containerRef.current) {
7      const { width, height } = containerRef.current.getBoundingClientRect();
8      setDimensions({ width, height });
9    }
10  }, []);
11
12  return (
13    <div ref={containerRef} className="gallery">
14      <p>Largeur : {dimensions.width}px</p>
15      <p>Hauteur : {dimensions.height}px</p>
16    </div>
17  );
18}

Composants contrôlés vs non contrôlés

En React, il y a deux façons de gérer les formulaires : contrôlés et non contrôlés.

Composants contrôlés (avec état)

La valeur de l'input est contrôlée par l'état React :

1function ControlledForm() {
2  const [email, setEmail] = useState("");
3
4  const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
5    e.preventDefault();
6    console.log("Email :", email);
7  };
8
9  return (
10    <form onSubmit={handleSubmit}>
11      <input
12        type="email"
13        value={email}                        // ← Contrôlé par l'état
14        onChange={(e) => setEmail(e.target.value)}  // ← Synchronisation
15      />
16      <button type="submit">Envoyer</button>
17    </form>
18  );
19}

Avantages du contrôlé :

  • Tu peux valider en temps réel
  • Tu peux modifier la valeur par programmation
  • Tu peux facilement réinitialiser le formulaire

Inconvénients du contrôlé :

  • Plus de code (état, onChange)
  • Re-rendu à chaque frappe au clavier

Composants non contrôlés (avec useRef)

La valeur de l'input est gérée par le DOM, on la lit seulement à la soumission :

1function UncontrolledForm() {
2  const emailRef = useRef<HTMLInputElement>(null);
3
4  const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
5    e.preventDefault();
6    const email = emailRef.current?.value;  // ← Lire depuis le DOM
7    console.log("Email :", email);
8  };
9
10  return (
11    <form onSubmit={handleSubmit}>
12      <input
13        ref={emailRef}                      // ← Ref au lieu de value/onChange
14        type="email"
15        defaultValue=""                     // ← defaultValue au lieu de value
16      />
17      <button type="submit">Envoyer</button>
18    </form>
19  );
20}

Avantages du non contrôlé :

  • Moins de code
  • Meilleures performances (pas de re-rendus)
  • Intégration plus facile avec des bibliothèques non React

Inconvénients du non contrôlé :

  • Pas de validation en temps réel
  • Impossible de modifier la valeur par programmation
  • Réinitialisation du formulaire plus difficile

Quand utiliser lequel ?

Contrôlé :

  • Validation en direct (par exemple « L'email doit contenir @ »)
  • Formatage en direct (par exemple un numéro de téléphone : (123) 456-7890)
  • Logique conditionnelle (par exemple afficher un autre champ si une case est cochée)
  • Synchronisation avec d'autres composants

Non contrôlé :

  • Soumission simple d'un formulaire (par exemple connexion, newsletter)
  • Meilleures performances pour les grands formulaires
  • Intégration avec des bibliothèques externes
  • Envoi de fichiers (<input type="file"> est toujours non contrôlé !)

Exemple pratique - Formulaire de connexion des pirates

Version contrôlée

Chaque champ a son propre état, et la fonction validate vérifie les données à l'envoi et affiche un message sous le champ incorrect :

1function ControlledLoginForm() {
2  const [username, setUsername] = useState("");
3  const [password, setPassword] = useState("");
4  const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
5
6  const validate = () => {
7    const newErrors: typeof errors = {};
8
9    if (username.length < 3) {
10      newErrors.username = "Le nom d'utilisateur doit faire au moins 3 caractères";
11    }
12    if (password.length < 6) {
13      newErrors.password = "Le mot de passe doit faire au moins 6 caractères";
14    }
15
16    setErrors(newErrors);
17    return Object.keys(newErrors).length === 0;
18  };
19
20  const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
21    e.preventDefault();
22    if (validate()) {
23      console.log("Connexion :", { username, password });
24      // Appel API...
25    }
26  };
27
28  return (
29    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
30      <div>
31        <label className="block mb-2 font-medium">Nom d'utilisateur</label>
32        <input
33          type="text"
34          value={username}
35          onChange={(e) => setUsername(e.target.value)}
36          className="w-full border rounded px-3 py-2"
37        />
38        {errors.username && (
39          <p className="text-red-600 text-sm mt-1">{errors.username}</p>
40        )}
41      </div>
42
43      <div>
44        <label className="block mb-2 font-medium">Mot de passe</label>
45        <input
46          type="password"
47          value={password}
48          onChange={(e) => setPassword(e.target.value)}
49          className="w-full border rounded px-3 py-2"
50        />
51        {errors.password && (
52          <p className="text-red-600 text-sm mt-1">{errors.password}</p>
53        )}
54      </div>
55
56      <button
57        type="submit"
58        className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700"
59      >
60        Se connecter
61      </button>
62    </form>
63  );
64}

Version non contrôlée

Ici, les champs n'ont pas d'état. On lit les valeurs dans les refs seulement après l'envoi du formulaire, donc la saisie ne provoque pas de rendu :

1function UncontrolledLoginForm() {
2  const usernameRef = useRef<HTMLInputElement>(null);
3  const passwordRef = useRef<HTMLInputElement>(null);
4  const [error, setError] = useState("");
5
6  const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
7    e.preventDefault();
8
9    const username = usernameRef.current?.value || "";
10    const password = passwordRef.current?.value || "";
11
12    // Validation à la soumission
13    if (username.length < 3) {
14      setError("Le nom d'utilisateur doit faire au moins 3 caractères");
15      return;
16    }
17    if (password.length < 6) {
18      setError("Le mot de passe doit faire au moins 6 caractères");
19      return;
20    }
21
22    setError("");
23    console.log("Connexion :", { username, password });
24    // Appel API...
25  };
26
27  return (
28    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4">
29      {error && (
30        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
31          {error}
32        </div>
33      )}
34
35      <div>
36        <label className="block mb-2 font-medium">Nom d'utilisateur</label>
37        <input
38          ref={usernameRef}
39          type="text"
40          defaultValue=""
41          className="w-full border rounded px-3 py-2"
42        />
43      </div>
44
45      <div>
46        <label className="block mb-2 font-medium">Mot de passe</label>
47        <input
48          ref={passwordRef}
49          type="password"
50          defaultValue=""
51          className="w-full border rounded px-3 py-2"
52        />
53      </div>
54
55      <button
56        type="submit"
57        className="w-full bg-blue-600 text-white py-2 rounded font-medium hover:bg-blue-700"
58      >
59        Se connecter
60      </button>
61    </form>
62  );
63}

Envoi de fichiers - Toujours non contrôlé

<input type="file"> est toujours non contrôlé - React ne peut pas contrôler les fichiers pour des raisons de sécurité.

1function AvatarUpload() {
2  const fileInputRef = useRef<HTMLInputElement>(null);
3  const [preview, setPreview] = useState<string | null>(null);
4
5  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6    const file = e.target.files?.[0];
7    if (file) {
8      // Aperçu de l'image
9      const reader = new FileReader();
10      reader.onloadend = () => {
11        setPreview(reader.result as string);
12      };
13      reader.readAsDataURL(file);
14    }
15  };
16
17  const handleUpload = () => {
18    const file = fileInputRef.current?.files?.[0];
19    if (file) {
20      console.log("Envoi :", file.name);
21      // Envoi vers l'API...
22    }
23  };
24
25  return (
26    <div className="space-y-4">
27      <input
28        ref={fileInputRef}
29        type="file"
30        accept="image/*"
31        onChange={handleFileChange}
32        className="block w-full text-sm"
33      />
34
35      {preview && (
36        <img
37          src={preview}
38          alt="Aperçu"
39          className="w-32 h-32 rounded-full object-cover"
40        />
41      )}
42
43      <button
44        onClick={handleUpload}
45        className="bg-blue-600 text-white px-4 py-2 rounded"
46      >
47        Envoyer l'avatar
48      </button>
49    </div>
50  );
51}

useRef pour des valeurs auxiliaires

useRef est idéal pour stocker des valeurs qui n'affectent pas l'UI mais qui doivent survivre entre les rendus.

Exemple 1 : La valeur précédente de l'état

Le hook personnalisé usePrevious garde dans une ref la valeur du rendu précédent. L'effet enregistre la nouvelle valeur seulement après le rendu, donc pendant le rendu la ref contient encore l'ancienne :

1function usePrevious<T>(value: T): T | undefined {
2  const ref = useRef<T | undefined>(undefined);
3
4  useEffect(() => {
5    ref.current = value;
6  }, [value]);
7
8  return ref.current;
9}
10
11// Utilisation
12function Counter() {
13  const [count, setCount] = useState(0);
14  const prevCount = usePrevious(count);
15
16  return (
17    <div>
18      <p>Actuel : {count}</p>
19      <p>Précédent : {prevCount}</p>
20      <button onClick={() => setCount(count + 1)}>
21        Incrémenter
22      </button>
23    </div>
24  );
25}

Ce pattern lit ref.current pendant le rendu, c'est-à-dire exactement ce que la documentation de React interdit. Tu le croiseras dans de nombreux projets et il fonctionne en général, mais dans du nouveau code, mieux vaut garder la valeur précédente dans l'état.

Exemple 2 : Les IDs d'Interval/Timeout

L'identifiant renvoyé par setInterval n'est pas utile à l'écran, mais la fonction stop doit le connaître. C'est pourquoi on le garde dans une ref, et non dans l'état :

1function Timer() {
2  const [seconds, setSeconds] = useState(0);
3  const intervalRef = useRef<NodeJS.Timeout | null>(null);
4
5  const start = () => {
6    if (intervalRef.current) return;  // Déjà en cours
7
8    intervalRef.current = setInterval(() => {
9      setSeconds(s => s + 1);
10    }, 1000);
11  };
12
13  const stop = () => {
14    if (intervalRef.current) {
15      clearInterval(intervalRef.current);
16      intervalRef.current = null;
17    }
18  };
19
20  const reset = () => {
21    stop();
22    setSeconds(0);
23  };
24
25  // Nettoyage au démontage
26  useEffect(() => {
27    return () => stop();
28  }, []);
29
30  return (
31    <div>
32      <p className="text-4xl font-bold">{seconds}s</p>
33      <div className="space-x-2">
34        <button onClick={start}>Démarrer</button>
35        <button onClick={stop}>Arrêter</button>
36        <button onClick={reset}>Réinitialiser</button>
37      </div>
38    </div>
39  );
40}

Exemple 3 : Une valeur périmée dans un callback

Un callback créé dans un effet avec un tableau de dépendances vide voit les valeurs du premier rendu :

1function Chat() {
2  const [messages, setMessages] = useState<string[]>([]);
3
4  useEffect(() => {
5    // Callback WebSocket
6    const ws = new WebSocket('ws://localhost:8080');
7
8    ws.onmessage = (event) => {
9      // Mauvais : messages du premier rendu
10      // setMessages([...messages, event.data]);
11
12      // Bon : la fonction de mise à jour reçoit la liste actuelle
13      setMessages((prev) => [...prev, event.data]);
14    };
15
16    return () => ws.close();
17  }, []);  // Tableau vide - l'effet ne s'exécute qu'une fois !
18
19  return (
20    <div>
21      {messages.map((msg, i) => <p key={i}>{msg}</p>)}
22    </div>
23  );
24}

Pour mettre à jour l'état, tu n'as pas besoin de ref. Une ref qui contient la valeur actuelle (valueRef.current = value dans un effet) n'est utile que si le callback se contente de lire cette valeur, par exemple pour l'envoyer au serveur.

Passer une ref à ton propre composant

Parfois, tu veux passer une ref à un composant enfant, par exemple pour donner le focus à un champ qui se trouve à l'intérieur. Dans React 19, ref est une prop ordinaire d'un composant fonction, il suffit donc de la transmettre à l'élément du DOM :

1function MyInput(props: React.ComponentProps<'input'>) {
2  return <input {...props} />;
3}
4
5function Form() {
6  const inputRef = useRef<HTMLInputElement>(null);
7
8  return (
9    <div>
10      <MyInput ref={inputRef} placeholder="Écris ici..." />
11      <button onClick={() => inputRef.current?.focus()}>
12        Focus
13      </button>
14    </div>
15  );
16}

React.ComponentProps<'input'> est le type de toutes les props de la balise <input>, y compris ref. Avant React 19, ce code ne fonctionnait pas et il fallait envelopper le composant dans forwardRef. Tu le croiseras encore dans d'anciens projets, mais dans React 19 il n'est plus nécessaire, et React a annoncé qu'il le marquera comme obsolète à l'avenir.

useImperativeHandle - Une API de ref personnalisée

useImperativeHandle te permet de personnaliser ce qui est disponible via la ref. Dans React 19, le composant reçoit ref comme une prop ordinaire, et le hook décide de ce que le parent verra dans ref.current :

1import { useImperativeHandle, useRef } from 'react';
2
3interface VideoPlayerRef {
4  play: () => void;
5  pause: () => void;
6  seek: (time: number) => void;
7}
8
9function VideoPlayer({ src, ref }: { src: string; ref?: React.Ref<VideoPlayerRef> }) {
10  const videoRef = useRef<HTMLVideoElement>(null);
11
12  useImperativeHandle(ref, () => ({
13    play: () => videoRef.current?.play(),
14    pause: () => videoRef.current?.pause(),
15    seek: (time: number) => {
16      if (videoRef.current) {
17        videoRef.current.currentTime = time;
18      }
19    }
20  }));
21
22  return <video ref={videoRef} src={src} />;
23}
24
25// Utilisation
26function App() {
27  const playerRef = useRef<VideoPlayerRef>(null);
28
29  return (
30    <div>
31      <VideoPlayer ref={playerRef} src="/video.mp4" />
32
33      <button onClick={() => playerRef.current?.play()}>Lecture</button>
34      <button onClick={() => playerRef.current?.pause()}>Pause</button>
35      <button onClick={() => playerRef.current?.seek(30)}>
36        Aller à 30s
37      </button>
38    </div>
39  );
40}

useImperativeHandle limite l'API - le parent ne voit que play, pause, seek, pas l'élément vidéo tout entier.

Récapitulatif

  • useRef - hook pour créer des références, ne provoque pas de re-rendu
  • useRef(initialValue) - retourne { current: initialValue }
  • ref={myRef} - assigne un élément du DOM à .current
  • Composants contrôlés - valeur contrôlée par l'état (value + onChange)
  • Composants non contrôlés - valeur dans le DOM, lue via useRef
  • Inputs de fichiers - toujours non contrôlés
  • useRef pour des valeurs - stocker des valeurs sans re-rendu (minuteurs, valeurs précédentes)
  • ref comme prop - dans React 19, tu passes une ref à ton propre composant comme une prop ordinaire (forwardRef seulement dans l'ancien code)
  • useImperativeHandle - une API de ref personnalisée

Quand utiliser :

  • useState - pour les données de l'UI qui provoquent un re-rendu
  • useRef (DOM) - pour accéder aux éléments du DOM
  • useRef (valeurs) - pour des valeurs auxiliaires (minuteurs, état précédent)

Dans l'éditeur ci-dessous, tu peux tester trois usages de cette leçon : le focus donné via une ref, un compteur dans une ref qui ne rafraîchit pas l'écran et un champ non contrôlé lu seulement à l'envoi.

On se retrouve au module 3 !

Code de cette leçon : App.jsx
1// useRef : focus, une valeur sans re-rendu et un champ non contrôlé
2import { useRef, useState } from 'react';
3
4export default function App() {
5  const inputRef = useRef(null);
6  const clicksRef = useRef(0);
7  const [log, setLog] = useState('');
8
9  const handleFocus = () => {
10    inputRef.current.focus();
11  };
12
13  const handleRefClick = () => {
14    // Modifier une ref ne met pas l'écran à jour
15    clicksRef.current = clicksRef.current + 1;
16  };
17
18  const handleSubmit = (e) => {
19    e.preventDefault();
20    // Champ non contrôlé : on lit la valeur dans le DOM seulement à l'envoi
21    setLog('Message : ' + inputRef.current.value + ' (clics enregistrés dans la ref : ' + clicksRef.current + ')');
22    inputRef.current.value = '';
23  };
24
25  return (
26    <div className="min-h-screen bg-slate-900 p-8 text-white">
27      <h1 className="text-3xl font-bold mb-6">useRef à bord</h1>
28      <form onSubmit={handleSubmit} className="space-y-4">
29        <input
30          ref={inputRef}
31          defaultValue=""
32          placeholder="Message à l'équipage"
33          className="w-full p-3 rounded text-black"
34        />
35        <div className="space-x-2">
36          <button type="button" onClick={handleFocus} className="bg-blue-600 px-4 py-2 rounded">
37            Donner le focus
38          </button>
39          <button type="button" onClick={handleRefClick} className="bg-amber-600 px-4 py-2 rounded">
40            +1 dans la ref (l'écran ne changera pas)
41          </button>
42          <button type="submit" className="bg-green-600 px-4 py-2 rounded">
43            Envoyer
44          </button>
45        </div>
46      </form>
47      <p className="mt-6">{log}</p>
48    </div>
49  );
50}

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. 1. Que retourne l'appel useRef(0) ?

  2. 2. En quoi useRef diffère-t-il de useState quand sa valeur change ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne les éléments qui donnent le focus à l'élément contenu dans inputRef :

Articles utiles