Cours JavaScript et React · Module 10: Écosystème et avenir de React
React 19 - Une nouvelle façon de passer ref (sans forwardRef)
Dans cette leçon6
Le panneau de navigation veut placer le focus sur un champ de coordonnées caché dans un composant enfant. ref donne justement ce genre d'accès direct à un élément du DOM, mais pendant des années, attaché à ton propre composant, il n'atteignait pas l'intérieur : React le traitait de façon spéciale et ne le transmettait pas dans les props. Il fallait envelopper le composant dans forwardRef, comme dans un relais de signal supplémentaire. Avec React 19, ce changement tant attendu est enfin arrivé, tout comme les nouveaux systèmes de communication des vaisseaux éliminent les relais compliqués.
L'ancienne méthode avec forwardRef (React 18 et avant)
Dans les versions précédentes de React, pour passer une ref à un composant enfant, il fallait utiliser forwardRef. La fonction du composant recevait ref en deuxième argument, à côté des props :
1import { forwardRef, useRef } from 'react';
2
3// Ancienne méthode - nécessite forwardRef
4const SpaceshipButton = forwardRef((props, ref) => {
5 return (
6 <button ref={ref} className="spaceship-btn" {...props}>
7 {props.children}
8 </button>
9 );
10});
11
12// Utilisation
13function MissionControl() {
14 const buttonRef = useRef(null);
15
16 const focusButton = () => {
17 buttonRef.current?.focus();
18 };
19
20 return (
21 <div>
22 <SpaceshipButton ref={buttonRef}>
23 Démarrer les moteurs
24 </SpaceshipButton>
25 <button onClick={focusButton}>Focus sur le bouton</button>
26 </div>
27 );
28}MissionControl crée buttonRef avec useRef et, au clic, appelle focus() sur le vrai bouton. Ça fonctionne, mais le composant a une signature inhabituelle (props, ref) et une enveloppe supplémentaire dont il faut se souvenir pour chaque composant de ce type.
La nouvelle méthode dans React 19 - ref comme prop
Dans React 19, ref est une prop ordinaire d'un composant fonctionnel. Tu la récupères dans les props comme n'importe quelle autre valeur :
1import { useRef } from 'react';
2
3// Nouvelle méthode dans React 19 - ref est simplement une prop !
4function SpaceshipButton({ ref, children, ...props }) {
5 return (
6 <button ref={ref} className="spaceship-btn" {...props}>
7 {children}
8 </button>
9 );
10}
11
12// Utilisation - exactement la même !
13function MissionControl() {
14 const buttonRef = useRef(null);
15
16 const focusButton = () => {
17 buttonRef.current?.focus();
18 };
19
20 return (
21 <div>
22 <SpaceshipButton ref={buttonRef}>
23 Démarrer les moteurs
24 </SpaceshipButton>
25 <button onClick={focusButton}>Focus sur le bouton</button>
26 </div>
27 );
28}Seule la définition a changé : forwardRef disparaît, et ref rejoint la déstructuration à côté de children. L'utilisation dans MissionControl est exactement la même. La règle concerne les composants fonctionnels, car dans les composants de classe ref désigne toujours l'instance et n'arrive pas dans les props.
Exemples pratiques
1. Input avec auto-focus
Le formulaire de coordonnées montre comment ref facilite la navigation au clavier. Chaque CoordinateInput transmet ref directement à l'élément input :
1// React 19 - un simple composant d'input
2function CoordinateInput({ ref, label, ...props }) {
3 return (
4 <div className="coordinate-field">
5 <label>{label}</label>
6 <input ref={ref} {...props} />
7 </div>
8 );
9}
10
11function NavigationPanel() {
12 const latitudeRef = useRef(null);
13 const longitudeRef = useRef(null);
14
15 useEffect(() => {
16 // Auto-focus sur le premier input
17 latitudeRef.current?.focus();
18 }, []);
19
20 const handleLatitudeEnter = (e) => {
21 if (e.key === 'Enter') {
22 longitudeRef.current?.focus();
23 }
24 };
25
26 return (
27 <form className="nav-panel">
28 <h2>Saisis les coordonnées de destination</h2>
29
30 <CoordinateInput
31 ref={latitudeRef}
32 label="Latitude galactique"
33 onKeyDown={handleLatitudeEnter}
34 />
35
36 <CoordinateInput
37 ref={longitudeRef}
38 label="Longitude galactique"
39 />
40
41 <button type="submit">Fixer le cap</button>
42 </form>
43 );
44}Après le montage, l'effet place le focus sur le champ de latitude, et Entrée le déplace vers la longitude. CoordinateInput reste une simple fonction sans enveloppe, et les autres props, comme onKeyDown, arrivent à l'input via ...props.
useImperativeHandle - toujours utile
Parfois, tu ne veux pas donner au parent tout l'élément du DOM, seulement certaines commandes. useImperativeHandle fonctionne toujours, mais désormais sans forwardRef : tu lui passes la ref reçue dans les props :
1import { useImperativeHandle, useRef } from 'react';
2
3function VideoPlayer({ ref, src }) {
4 const videoRef = useRef(null);
5
6 // N'exposer que certaines méthodes
7 useImperativeHandle(ref, () => ({
8 play: () => videoRef.current?.play(),
9 pause: () => videoRef.current?.pause(),
10 seek: (time) => {
11 if (videoRef.current) {
12 videoRef.current.currentTime = time;
13 }
14 },
15 getCurrentTime: () => videoRef.current?.currentTime || 0,
16 }), []);
17
18 return (
19 <video ref={videoRef} src={src} className="space-video">
20 Ton navigateur ne prend pas en charge la vidéo.
21 </video>
22 );
23}
24
25function MediaController() {
26 const playerRef = useRef(null);
27
28 return (
29 <div>
30 <VideoPlayer ref={playerRef} src="/space-documentary.mp4" />
31
32 <div className="controls">
33 <button onClick={() => playerRef.current?.play()}>
34 Lecture
35 </button>
36 <button onClick={() => playerRef.current?.pause()}>
37 Pause
38 </button>
39 <button onClick={() => playerRef.current?.seek(0)}>
40 Depuis le début
41 </button>
42 </div>
43 </div>
44 );
45}Le parent ne voit que play, pause, seek et getCurrentTime, et pas tout l'élément video. C'est une API plus sûre, car personne à l'extérieur ne modifiera par accident les attributs du lecteur.
Migration depuis forwardRef
Si tu as du code avec forwardRef, la migration est simple : tu déplaces ref du deuxième argument vers les props :
1// Avant (React 18)
2const OldComponent = forwardRef(function OldComponent(props, ref) {
3 return <div ref={ref}>{props.children}</div>;
4});
5
6// Après (React 19)
7function NewComponent({ ref, children }) {
8 return <div ref={ref}>{children}</div>;
9}
10
11// L'utilisation reste identique !
12function Panel() {
13 const oldRef = useRef(null);
14 const newRef = useRef(null);
15
16 return (
17 <>
18 <OldComponent ref={oldRef}>Ancien</OldComponent>
19 <NewComponent ref={newRef}>Nouveau</NewComponent>
20 </>
21 );
22}Les endroits où le composant est utilisé ne changent pas, donc tu peux migrer composant par composant. Rien ne presse non plus : forwardRef fonctionne toujours dans React 19 et n'est pas encore marqué comme déprécié (deprecated), la documentation l'annonce seulement pour une future version.
Résumé
React 19 apporte une simplification importante dans la transmission des références :
- ref est une prop ordinaire - dans les composants fonctionnels, elle ne demande aucun traitement spécial
- forwardRef n'est plus nécessaire - il fonctionne toujours, et la documentation annonce qu'il sera marqué comme déprécié (deprecated) dans une future version
- useImperativeHandle fonctionne sans forwardRef - passe ref comme prop
- Les callback refs fonctionnent comme avant - et depuis React 19, elles peuvent aussi renvoyer une fonction de nettoyage, que React appelle quand l'élément est détaché
- TypeScript - tu décris ref dans le type des props comme n'importe quelle autre prop, par exemple
React.Ref<HTMLInputElement>
Dans le nouveau code, je te recommande d'écrire tout de suite des composants avec ref dans les props, et de migrer l'ancien code à l'occasion d'autres modifications. Cela te servira dans chaque bibliothèque de composants, car les boutons et les champs doivent exposer une ref pour le focus et les mesures. Cette simplification rend les composants plus prévisibles et plus faciles à comprendre.
Retiens : dans React 19, ref est un canal de communication ordinaire entre les modules - tu le branches comme n'importe quelle autre prop, sans relais supplémentaire. Dans l'exemple ci-dessous, un ancien bouton avec forwardRef et de nouveaux composants avec ref dans leurs props fonctionnent côte à côte, et la boîte de dialogue expose des méthodes à son parent via useImperativeHandle.
Code de cette leçon : App.jsx
1import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
3// React 19 : ref est une prop ordinaire d’un composant fonctionnel.
4// forwardRef fonctionne toujours, mais n’est plus nécessaire.
5
6// L’ancienne méthode (React 18) - fonctionne toujours dans React 19
7const OldSpaceButton = forwardRef(function OldSpaceButton(props, ref) {
8 return <button ref={ref} style={{ ...styles.button, background: '#778da9' }} {...props} />;
9});
10
11// La nouvelle méthode dans React 19 - tu récupères ref dans les props comme n’importe quelle valeur
12function SpaceButton({ ref, children, variant = 'primary', ...props }) {
13 const colors = {
14 primary: { background: '#00d4ff', color: '#0a0e27' },
15 danger: { background: '#ff6b6b', color: '#fff' },
16 success: { background: '#00ff88', color: '#0a0e27' },
17 };
18
19 return (
20 <button ref={ref} style={{ ...styles.button, ...(colors[variant] || colors.primary) }} {...props}>
21 {children}
22 </button>
23 );
24}
25
26function SpaceInput({ ref, label, ...props }) {
27 return (
28 <label style={styles.field}>
29 <span style={styles.label}>{label}</span>
30 <input ref={ref} style={styles.input} {...props} />
31 </label>
32 );
33}
34
35// useImperativeHandle avec la ref des props : le parent ne reçoit que open, close et toggle
36function SpaceDialog({ ref, title, children }) {
37 const [isOpen, setIsOpen] = useState(false);
38
39 useImperativeHandle(ref, () => ({
40 open: () => setIsOpen(true),
41 close: () => setIsOpen(false),
42 toggle: () => setIsOpen(prev => !prev),
43 }), []);
44
45 if (!isOpen) return null;
46
47 return (
48 <div style={styles.overlay}>
49 <div style={styles.dialog}>
50 <h3 style={{ color: '#00d4ff', margin: '0 0 12px' }}>{title}</h3>
51 {children}
52 <button onClick={() => setIsOpen(false)} style={styles.closeButton}>Fermer</button>
53 </div>
54 </div>
55 );
56}
57
58export default function App() {
59 const buttonRef = useRef(null);
60 const oldButtonRef = useRef(null);
61 const inputRef = useRef(null);
62 const dialogRef = useRef(null);
63 const [log, setLog] = useState([]);
64
65 const addLog = message => setLog(prev => [message, ...prev].slice(0, 6));
66
67 return (
68 <div style={styles.page}>
69 <div style={{ maxWidth: '600px', margin: '0 auto' }}>
70 <h1 style={styles.title}>React 19 : ref en tant que prop</h1>
71 <p style={styles.subtitle}>forwardRef n’est plus nécessaire, même s’il fonctionne toujours</p>
72
73 <section style={styles.card}>
74 <h3 style={styles.cardTitle}>Bouton avec ref dans ses props</h3>
75 <div style={styles.row}>
76 <SpaceButton ref={buttonRef} onClick={() => addLog('Clic sur le bouton avec ref')}>
77 Démarrer les moteurs
78 </SpaceButton>
79 <SpaceButton variant="success" onClick={() => { buttonRef.current?.focus(); addLog('Focus sur le bouton (nouvelle méthode)'); }}>
80 Placer le focus
81 </SpaceButton>
82 </div>
83 </section>
84
85 <section style={styles.card}>
86 <h3 style={styles.cardTitle}>Ancien bouton avec forwardRef</h3>
87 <div style={styles.row}>
88 <OldSpaceButton ref={oldButtonRef} onClick={() => addLog('Clic sur l’ancien bouton')}>
89 Ancien relais
90 </OldSpaceButton>
91 <SpaceButton variant="success" onClick={() => { oldButtonRef.current?.focus(); addLog('Focus sur le bouton (forwardRef)'); }}>
92 Placer le focus
93 </SpaceButton>
94 </div>
95 </section>
96
97 <section style={styles.card}>
98 <h3 style={styles.cardTitle}>Champ de texte avec ref dans ses props</h3>
99 <SpaceInput ref={inputRef} label="Nom de la mission" placeholder="Saisis un nom..." />
100 <SpaceButton onClick={() => { inputRef.current?.focus(); addLog('Focus sur le champ de texte'); }}>
101 Placer le focus sur le champ
102 </SpaceButton>
103 </section>
104
105 <section style={styles.card}>
106 <h3 style={styles.cardTitle}>Boîte de dialogue avec useImperativeHandle</h3>
107 <div style={styles.row}>
108 <SpaceButton onClick={() => { dialogRef.current?.open(); addLog('Boîte ouverte'); }}>Ouvrir la boîte</SpaceButton>
109 <SpaceButton variant="danger" onClick={() => { dialogRef.current?.close(); addLog('Boîte fermée'); }}>Fermer la boîte</SpaceButton>
110 </div>
111 </section>
112
113 <SpaceDialog ref={dialogRef} title="Message de la passerelle">
114 <p style={{ margin: 0 }}>Cette boîte n’expose à son parent que les méthodes open, close et toggle via useImperativeHandle.</p>
115 </SpaceDialog>
116
117 <section style={styles.card}>
118 <h3 style={styles.cardTitle}>Journal des événements</h3>
119 {log.length === 0 ? (
120 <p style={styles.muted}>Clique sur les boutons ci-dessus...</p>
121 ) : (
122 log.map((entry, i) => <div key={i} style={styles.logRow}>{entry}</div>)
123 )}
124 </section>
125 </div>
126 </div>
127 );
128}
129
130const styles = {
131 page: { minHeight: '100vh', background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)', padding: '24px', color: '#e0e1dd', fontFamily: 'system-ui, sans-serif' },
132 title: { textAlign: 'center', color: '#00d4ff', margin: '0 0 6px' },
133 subtitle: { textAlign: 'center', color: '#8892b0', margin: '0 0 18px', fontSize: '14px' },
134 card: { background: 'rgba(0,0,0,0.3)', padding: '16px', borderRadius: '12px', marginBottom: '12px' },
135 cardTitle: { color: '#64ffda', margin: '0 0 10px', fontSize: '15px' },
136 row: { display: 'flex', gap: '8px', flexWrap: 'wrap' },
137 button: { padding: '9px 16px', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold', fontSize: '14px', color: '#0a0e27' },
138 field: { display: 'block', marginBottom: '10px' },
139 label: { display: 'block', color: '#8892b0', fontSize: '13px', marginBottom: '4px' },
140 input: { width: '100%', boxSizing: 'border-box', padding: '9px 12px', background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(0,212,255,0.3)', borderRadius: '8px', color: '#e0e1dd', fontSize: '14px' },
141 overlay: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 },
142 dialog: { background: '#1a1a3e', padding: '24px', borderRadius: '12px', border: '1px solid rgba(0,212,255,0.3)', maxWidth: '400px', width: '90%' },
143 closeButton: { marginTop: '16px', padding: '8px 16px', background: 'rgba(255,255,255,0.1)', color: '#e0e1dd', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', cursor: 'pointer' },
144 muted: { color: '#8892b0', fontSize: '13px', margin: 0 },
145 logRow: { fontSize: '12px', color: '#8892b0', padding: '2px 0', fontFamily: 'monospace' },
146};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. Comment passe-t-on une ref à un composant fonctionnel dans React 19 ?
2. Quel est le statut de forwardRef dans React 19 ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Compose la nouvelle façon de définir un composant avec ref dans React 19 :