Kurs JavaScript i React · Moduł 10: Ekosystem i przyszłość React
React 19 - Nowy sposób przekazywania ref (bez forwardRef)
W tej lekcji6
Panel nawigacyjny chce ustawić fokus na polu współrzędnych ukrytym w komponencie potomnym. ref daje właśnie taki bezpośredni dostęp do elementu DOM, ale przypięty do własnego komponentu przez lata nie docierał do środka: React traktował go specjalnie i nie przekazywał w propsach. Trzeba było owijać komponent w forwardRef, jak w dodatkowy przekaźnik sygnału. W React 19 ta długo oczekiwana zmiana wreszcie nadeszła, podobnie jak nowe systemy komunikacji na statkach eliminują skomplikowane przekaźniki.
Stary sposób z forwardRef (React 18 i wcześniej)
W poprzednich wersjach Reacta, żeby przekazać ref do komponentu potomnego, trzeba było użyć forwardRef. Funkcja komponentu dostawała ref jako drugi argument, obok propsów:
1import { forwardRef, useRef } from 'react';
2
3// Stary sposób - wymaga 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// Użycie
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 Uruchom silniki
24 </SpaceshipButton>
25 <button onClick={focusButton}>Fokus na przycisk</button>
26 </div>
27 );
28}MissionControl tworzy buttonRef przez useRef i po kliknięciu wywołuje focus() na prawdziwym przycisku. Działa, ale komponent ma nietypową sygnaturę (props, ref) i dodatkowe opakowanie, o którym trzeba pamiętać przy każdym takim komponencie.
Nowy sposób w React 19 - ref jako prop
W React 19 ref jest zwykłym propem komponentu funkcyjnego. Wyciągasz go z propsów jak każdą inną wartość:
1import { useRef } from 'react';
2
3// Nowy sposób w React 19 - ref to po prostu prop!
4function SpaceshipButton({ ref, children, ...props }) {
5 return (
6 <button ref={ref} className="spaceship-btn" {...props}>
7 {children}
8 </button>
9 );
10}
11
12// Użycie - dokładnie tak samo!
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 Uruchom silniki
24 </SpaceshipButton>
25 <button onClick={focusButton}>Fokus na przycisk</button>
26 </div>
27 );
28}Zmieniła się tylko definicja: znika forwardRef, a ref trafia do destrukturyzacji obok children. Użycie w MissionControl jest dokładnie takie samo. Zasada dotyczy komponentów funkcyjnych, bo w klasowych ref nadal wskazuje instancję i nie trafia do propsów.
Praktyczne przykłady
1. Input z auto-focus
Formularz współrzędnych pokazuje, jak ref ułatwia nawigację klawiaturą. Każdy CoordinateInput przekazuje ref prosto do elementu input:
1// React 19 - prosty komponent inputa
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 na pierwszy 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>Wprowadź współrzędne docelowe</h2>
29
30 <CoordinateInput
31 ref={latitudeRef}
32 label="Szerokość galaktyczna"
33 onKeyDown={handleLatitudeEnter}
34 />
35
36 <CoordinateInput
37 ref={longitudeRef}
38 label="Długość galaktyczna"
39 />
40
41 <button type="submit">Ustaw kurs</button>
42 </form>
43 );
44}Po zamontowaniu efekt ustawia fokus na polu szerokości, a Enter przenosi go do długości. CoordinateInput pozostaje zwykłą funkcją bez opakowania, a pozostałe propsy, jak onKeyDown, trafiają do inputa przez ...props.
useImperativeHandle - nadal użyteczny
Czasem nie chcesz oddawać rodzicowi całego elementu DOM, tylko wybrane polecenia. useImperativeHandle nadal działa, ale teraz bez forwardRef: podajesz mu ref odebrany z propsów:
1import { useImperativeHandle, useRef } from 'react';
2
3function VideoPlayer({ ref, src }) {
4 const videoRef = useRef(null);
5
6 // Eksponuj tylko wybrane metody
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 Twoja przeglądarka nie obsługuje wideo.
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 Odtwórz
35 </button>
36 <button onClick={() => playerRef.current?.pause()}>
37 Pauza
38 </button>
39 <button onClick={() => playerRef.current?.seek(0)}>
40 Od początku
41 </button>
42 </div>
43 </div>
44 );
45}Rodzic widzi tylko play, pause, seek i getCurrentTime, a nie cały element video. To bezpieczniejsze API, bo nikt z zewnątrz nie zmieni przypadkiem atrybutów odtwarzacza.
Migracja z forwardRef
Jeśli masz kod z forwardRef, migracja jest prosta: przenosisz ref z drugiego argumentu do propsów:
1// Przed (React 18)
2const OldComponent = forwardRef(function OldComponent(props, ref) {
3 return <div ref={ref}>{props.children}</div>;
4});
5
6// Po (React 19)
7function NewComponent({ ref, children }) {
8 return <div ref={ref}>{children}</div>;
9}
10
11// Użycie pozostaje identyczne!
12function Panel() {
13 const oldRef = useRef(null);
14 const newRef = useRef(null);
15
16 return (
17 <>
18 <OldComponent ref={oldRef}>Stary</OldComponent>
19 <NewComponent ref={newRef}>Nowy</NewComponent>
20 </>
21 );
22}Miejsca użycia się nie zmieniają, więc możesz migrować komponent po komponencie. Nie musisz się też spieszyć: forwardRef w React 19 nadal działa i nie jest jeszcze oznaczony jako przestarzały (deprecated), dokumentacja zapowiada to dopiero w przyszłej wersji.
Podsumowanie
React 19 wprowadza znaczące uproszczenie w przekazywaniu referencji:
- ref jest zwykłym propem - w komponentach funkcyjnych nie wymaga specjalnego traktowania
- forwardRef nie jest już potrzebny - nadal działa, a dokumentacja zapowiada, że w przyszłej wersji zostanie oznaczony jako przestarzały (deprecated)
- useImperativeHandle działa bez forwardRef - przekaż ref jako prop
- Callback refs działają jak dawniej - a od React 19 mogą też zwrócić funkcję sprzątającą, którą React wywoła przy odłączeniu elementu
- TypeScript - ref opisujesz w typie propsów jak każdy inny prop, np.
React.Ref<HTMLInputElement>
W nowym kodzie polecam Ci od razu pisać komponenty z ref w propsach, a stary kod migrować przy okazji innych zmian. Przyda się to w każdej bibliotece komponentów, bo przyciski i pola muszą udostępniać ref do fokusu i pomiarów. To uproszczenie sprawia, że komponenty są bardziej przewidywalne i łatwiejsze do zrozumienia.
Pamiętaj: w React 19 ref to zwykły kanał łączności między modułami - podłączasz go jak każdy inny prop, bez dodatkowego przekaźnika. W przykładzie poniżej stary przycisk z forwardRef i nowe komponenty z ref w propsach działają obok siebie, a okno dialogowe udostępnia rodzicowi metody przez useImperativeHandle.
Kod do tej lekcji: App.jsx
1import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
3// React 19: ref jest zwykłym propem komponentu funkcyjnego.
4// forwardRef nadal działa, ale nie jest już potrzebny.
5
6// Stary sposób (React 18) - wciąż działa w React 19
7const OldSpaceButton = forwardRef(function OldSpaceButton(props, ref) {
8 return <button ref={ref} style={{ ...styles.button, background: '#778da9' }} {...props} />;
9});
10
11// Nowy sposób w React 19 - ref wyciągasz z propsów jak każdą inną wartość
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 z ref z propsów: rodzic dostaje tylko open, close i 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}>Zamknij</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 jako prop</h1>
71 <p style={styles.subtitle}>forwardRef nie jest już potrzebny, choć nadal działa</p>
72
73 <section style={styles.card}>
74 <h3 style={styles.cardTitle}>Przycisk z ref w propsach</h3>
75 <div style={styles.row}>
76 <SpaceButton ref={buttonRef} onClick={() => addLog('Kliknięto przycisk z ref')}>
77 Uruchom silniki
78 </SpaceButton>
79 <SpaceButton variant="success" onClick={() => { buttonRef.current?.focus(); addLog('Fokus na przycisku (nowy sposób)'); }}>
80 Ustaw fokus
81 </SpaceButton>
82 </div>
83 </section>
84
85 <section style={styles.card}>
86 <h3 style={styles.cardTitle}>Stary przycisk z forwardRef</h3>
87 <div style={styles.row}>
88 <OldSpaceButton ref={oldButtonRef} onClick={() => addLog('Kliknięto stary przycisk')}>
89 Stary przekaźnik
90 </OldSpaceButton>
91 <SpaceButton variant="success" onClick={() => { oldButtonRef.current?.focus(); addLog('Fokus na przycisku (forwardRef)'); }}>
92 Ustaw fokus
93 </SpaceButton>
94 </div>
95 </section>
96
97 <section style={styles.card}>
98 <h3 style={styles.cardTitle}>Pole tekstowe z ref w propsach</h3>
99 <SpaceInput ref={inputRef} label="Nazwa misji" placeholder="Wpisz nazwę..." />
100 <SpaceButton onClick={() => { inputRef.current?.focus(); addLog('Fokus na polu tekstowym'); }}>
101 Ustaw fokus na polu
102 </SpaceButton>
103 </section>
104
105 <section style={styles.card}>
106 <h3 style={styles.cardTitle}>Okno dialogowe z useImperativeHandle</h3>
107 <div style={styles.row}>
108 <SpaceButton onClick={() => { dialogRef.current?.open(); addLog('Otwarto okno'); }}>Otwórz okno</SpaceButton>
109 <SpaceButton variant="danger" onClick={() => { dialogRef.current?.close(); addLog('Zamknięto okno'); }}>Zamknij okno</SpaceButton>
110 </div>
111 </section>
112
113 <SpaceDialog ref={dialogRef} title="Komunikat z mostka">
114 <p style={{ margin: 0 }}>To okno udostępnia rodzicowi tylko metody open, close i toggle przez useImperativeHandle.</p>
115 </SpaceDialog>
116
117 <section style={styles.card}>
118 <h3 style={styles.cardTitle}>Dziennik zdarzeń</h3>
119 {log.length === 0 ? (
120 <p style={styles.muted}>Kliknij przyciski powyżej...</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};Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak przekazać ref do komponentu funkcyjnego w React 19?
2. Jaki jest status forwardRef w React 19?
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż nowy sposób definiowania komponentu z ref w React 19: