JavaScript and React course Β· Module 10: React Ecosystem and Future
React 19 - A New Way to Pass ref (Without forwardRef)
In this lesson6
The navigation panel wants to set focus on a coordinates field hidden in a child component. ref gives exactly this kind of direct access to a DOM element, but for years, when attached to your own component, it never reached the inside: React treated it specially and did not pass it in the props. You had to wrap the component in forwardRef, like an extra signal relay. In React 19 this long-awaited change has finally arrived, just as new communication systems on ships eliminate complicated relays.
The Old Way with forwardRef (React 18 and Earlier)
In previous versions of React, to pass a ref to a child component you had to use forwardRef. The component function received ref as a second argument, next to the props:
1import { forwardRef, useRef } from 'react';
2
3// Old way - requires 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// Usage
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 Start engines
24 </SpaceshipButton>
25 <button onClick={focusButton}>Focus on button</button>
26 </div>
27 );
28}MissionControl creates buttonRef with useRef and, on click, calls focus() on the real button. It works, but the component has an unusual (props, ref) signature and an extra wrapper that you have to remember for every such component.
The New Way in React 19 - ref as a Prop
In React 19 ref is a regular prop of a function component. You pull it out of the props like any other value:
1import { useRef } from 'react';
2
3// New way in React 19 - ref is just a prop!
4function SpaceshipButton({ ref, children, ...props }) {
5 return (
6 <button ref={ref} className="spaceship-btn" {...props}>
7 {children}
8 </button>
9 );
10}
11
12// Usage - exactly the same!
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 Start engines
24 </SpaceshipButton>
25 <button onClick={focusButton}>Focus on button</button>
26 </div>
27 );
28}Only the definition changed: forwardRef disappears, and ref goes into the destructuring next to children. The usage in MissionControl is exactly the same. The rule applies to function components, because in class components ref still points to the instance and does not end up in the props.
Practical Examples
1. Input with Auto-focus
The coordinates form shows how ref makes keyboard navigation easier. Each CoordinateInput passes ref straight to the input element:
1// React 19 - simple input component
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 on first 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>Enter destination coordinates</h2>
29
30 <CoordinateInput
31 ref={latitudeRef}
32 label="Galactic latitude"
33 onKeyDown={handleLatitudeEnter}
34 />
35
36 <CoordinateInput
37 ref={longitudeRef}
38 label="Galactic longitude"
39 />
40
41 <button type="submit">Set course</button>
42 </form>
43 );
44}After mounting, the effect sets focus on the latitude field, and Enter moves it to longitude. CoordinateInput stays a plain function without a wrapper, and the remaining props, like onKeyDown, reach the input through ...props.
useImperativeHandle - Still Useful
Sometimes you do not want to give the parent the whole DOM element, only selected commands. useImperativeHandle still works, but now without forwardRef: you pass it the ref received from the props:
1import { useImperativeHandle, useRef } from 'react';
2
3function VideoPlayer({ ref, src }) {
4 const videoRef = useRef(null);
5
6 // Expose only selected methods
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 Your browser does not support video.
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 Play
35 </button>
36 <button onClick={() => playerRef.current?.pause()}>
37 Pause
38 </button>
39 <button onClick={() => playerRef.current?.seek(0)}>
40 From the start
41 </button>
42 </div>
43 </div>
44 );
45}The parent sees only play, pause, seek and getCurrentTime, not the whole video element. It is a safer API, because nobody outside will accidentally change the player's attributes.
Migrating from forwardRef
If you have code with forwardRef, the migration is simple: you move ref from the second argument into the props:
1// Before (React 18)
2const OldComponent = forwardRef(function OldComponent(props, ref) {
3 return <div ref={ref}>{props.children}</div>;
4});
5
6// After (React 19)
7function NewComponent({ ref, children }) {
8 return <div ref={ref}>{children}</div>;
9}
10
11// Usage remains identical!
12function Panel() {
13 const oldRef = useRef(null);
14 const newRef = useRef(null);
15
16 return (
17 <>
18 <OldComponent ref={oldRef}>Old</OldComponent>
19 <NewComponent ref={newRef}>New</NewComponent>
20 </>
21 );
22}The call sites do not change, so you can migrate component by component. You do not have to hurry either: forwardRef still works in React 19 and is not marked as deprecated yet, the documentation only announces that for a future version.
Summary
React 19 brings a significant simplification to passing references:
- ref is a regular prop - in function components it does not need special treatment
- forwardRef is no longer needed - it still works, and the documentation announces that it will be marked as deprecated in a future version
- useImperativeHandle works without forwardRef - pass ref as a prop
- Callback refs work as before - and since React 19 they can also return a cleanup function, which React calls when the element is detached
- TypeScript - you describe ref in the props type like any other prop, e.g.
React.Ref<HTMLInputElement>
In new code I recommend writing components with ref in the props right away, and migrating old code along with other changes. This will come in handy in every component library, because buttons and fields have to expose a ref for focus and measurements. This simplification makes components more predictable and easier to understand.
Remember: in React 19 ref is a regular communication channel between modules - you connect it like any other prop, without an extra relay. In the example below an old button with forwardRef and new components with ref in their props work side by side, and the dialog exposes methods to its parent through useImperativeHandle.
Code for this lesson: App.jsx
1import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
3// React 19: ref is a regular prop of a function component.
4// forwardRef still works, but it is no longer needed.
5
6// The old way (React 18) - still works in React 19
7const OldSpaceButton = forwardRef(function OldSpaceButton(props, ref) {
8 return <button ref={ref} style={{ ...styles.button, background: '#778da9' }} {...props} />;
9});
10
11// The new way in React 19 - you take ref from the props like any other value
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 with ref from the props: the parent gets only open, close and 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}>Close</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 as a prop</h1>
71 <p style={styles.subtitle}>forwardRef is no longer needed, although it still works</p>
72
73 <section style={styles.card}>
74 <h3 style={styles.cardTitle}>Button with ref in its props</h3>
75 <div style={styles.row}>
76 <SpaceButton ref={buttonRef} onClick={() => addLog('Button with ref clicked')}>
77 Start engines
78 </SpaceButton>
79 <SpaceButton variant="success" onClick={() => { buttonRef.current?.focus(); addLog('Focus on the button (new way)'); }}>
80 Set focus
81 </SpaceButton>
82 </div>
83 </section>
84
85 <section style={styles.card}>
86 <h3 style={styles.cardTitle}>Old button with forwardRef</h3>
87 <div style={styles.row}>
88 <OldSpaceButton ref={oldButtonRef} onClick={() => addLog('Old button clicked')}>
89 Old relay
90 </OldSpaceButton>
91 <SpaceButton variant="success" onClick={() => { oldButtonRef.current?.focus(); addLog('Focus on the button (forwardRef)'); }}>
92 Set focus
93 </SpaceButton>
94 </div>
95 </section>
96
97 <section style={styles.card}>
98 <h3 style={styles.cardTitle}>Text field with ref in its props</h3>
99 <SpaceInput ref={inputRef} label="Mission name" placeholder="Type a name..." />
100 <SpaceButton onClick={() => { inputRef.current?.focus(); addLog('Focus on the text field'); }}>
101 Focus the field
102 </SpaceButton>
103 </section>
104
105 <section style={styles.card}>
106 <h3 style={styles.cardTitle}>Dialog with useImperativeHandle</h3>
107 <div style={styles.row}>
108 <SpaceButton onClick={() => { dialogRef.current?.open(); addLog('Dialog opened'); }}>Open dialog</SpaceButton>
109 <SpaceButton variant="danger" onClick={() => { dialogRef.current?.close(); addLog('Dialog closed'); }}>Close dialog</SpaceButton>
110 </div>
111 </section>
112
113 <SpaceDialog ref={dialogRef} title="Message from the bridge">
114 <p style={{ margin: 0 }}>This dialog exposes only the open, close and toggle methods to its parent through useImperativeHandle.</p>
115 </SpaceDialog>
116
117 <section style={styles.card}>
118 <h3 style={styles.cardTitle}>Event log</h3>
119 {log.length === 0 ? (
120 <p style={styles.muted}>Click the buttons above...</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};Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How do you pass a ref to a functional component in React 19?
2. What is the status of forwardRef in React 19?
Hands-on tasks in the game
- Click in order
Arrange the new way of defining a component with ref in React 19: