Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Przechowywanie wartości między renderowaniami z useRef
W tej lekcji6
Wyobraź sobie taką sytuację: chcesz zapamiętać identyfikator timera misji albo poprzednią prędkość statku. Zwykła zmienna w komponencie odpada, bo przy każdym renderowaniu funkcja komponentu wykonuje się od nowa i zmienna wraca do wartości początkowej. useState zapamięta wartość, ale każda jej zmiana wywoła ponowne renderowanie, choć ekran wcale nie musi się zmienić. Potrzebujemy czegoś pośrodku.
W kosmicznej podróży przez React, czasami potrzebujemy przechowywać informacje, które przetrwają między renderowaniami komponentu, ale nie powinny powodować ponownego renderowania. Hook useRef jest jak kapsuła ratunkowa - przechowuje dane, które możesz odzyskać w dowolnym momencie, bez względu na to, ile razy twój statek kosmiczny (komponent) zostanie przebudowany.
Czym jest useRef?
Hook useRef tworzy mutowalny obiekt referencyjny, którego właściwość .current jest inicjalizowana przekazaną wartością. Ważną cechą useRef jest to, że:
- Wartość przechowywana w referencji pozostaje taka sama między renderowaniami
- Zmiana wartości referencji NIE powoduje ponownego renderowania komponentu
- Działa jak "magazyn pamięci", do którego możesz się odwoływać w dowolnym momencie w cyklu życia komponentu
Dokumentacja react.dev dodaje dwa szczegóły. Po pierwsze, useRef przy każdym renderowaniu zwraca ten sam obiekt, a wartość początkowa jest używana tylko przy pierwszym renderze. Po drugie, .current zmieniasz zwykłym przypisaniem, bez żadnej funkcji typu setter.
useRef vs useState - kluczowe różnice
Porównajmy dwie kluczowe metody przechowywania danych w komponentach. W poniższym panelu prędkość trzymamy w stanie, a poprzednią prędkość w referencji, którą aktualizuje useEffect po każdej zmianie:
1function SpaceshipDashboard() {
2 // Stan - zmiany powodują ponowne renderowanie
3 const [speed, setSpeed] = useState(0);
4
5 // Referencja - zmiany NIE powodują ponownego renderowania
6 const previousSpeedRef = useRef(0);
7
8 // Przy zmianie prędkości, zapisujemy poprzednią wartość
9 useEffect(() => {
10 previousSpeedRef.current = speed;
11 }, [speed]);
12
13 const increaseSpeed = () => {
14 setSpeed(speed + 10); // To spowoduje ponowne renderowanie
15 };
16
17 return (
18 <div>
19 <p>Aktualna prędkość: {speed} km/h</p>
20 <p>Poprzednia prędkość: {previousSpeedRef.current} km/h</p>
21 <button onClick={increaseSpeed}>Zwiększ prędkość</button>
22 </div>
23 );
24}Kliknięcie zmienia stan, więc komponent renderuje się ponownie. W trakcie tego renderu referencja ma jeszcze starą wartość, bo efekt uruchamia się dopiero po wyrenderowaniu. Dlatego na ekranie widzisz poprzednią prędkość. Samo przypisanie do previousSpeedRef.current w efekcie niczego nie odświeża.
Ważne zastrzeżenie: react.dev zaleca, żeby nie czytać ani nie zapisywać ref.current w trakcie renderowania (wyjątkiem jest leniwa inicjalizacja). Ten wzorzec działa w prostym panelu, ale jest kruchy. Jeśli poprzednia wartość ma być widoczna na ekranie, dokumentacja poleca trzymać ją w stanie. Referencję zostaw na dane, których ekran nie pokazuje.
Typowe przypadki użycia useRef
1. Przechowywanie poprzednich wartości
Jednym z najpopularniejszych zastosowań useRef jest zapamiętywanie poprzednich wartości między renderowaniami. Tracker asteroidy liczy, o ile obiekt przesunął się od ostatniego renderu:
1function AsteroidTracker({ asteroid }) {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3 const previousPositionRef = useRef({ x: 0, y: 0 });
4
5 useEffect(() => {
6 // Zapisujemy aktualną pozycję jako poprzednią przed następną aktualizacją
7 previousPositionRef.current = position;
8 }, [position]);
9
10 // Obliczamy, jak daleko poruszył się asteroid
11 const distanceMoved = Math.sqrt(
12 Math.pow(position.x - previousPositionRef.current.x, 2) +
13 Math.pow(position.y - previousPositionRef.current.y, 2)
14 );
15
16 return (
17 <div>
18 <p>Aktualna pozycja: ({position.x}, {position.y})</p>
19 <p>Poprzednia pozycja: ({previousPositionRef.current.x}, {previousPositionRef.current.y})</p>
20 <p>Przebyta odległość: {distanceMoved}</p>
21 </div>
22 );
23}Mechanizm jest ten sam co w panelu prędkości: efekt zapisuje pozycję po renderze, a obliczenie korzysta z wartości z poprzedniego renderu. Obowiązuje tu też to samo zastrzeżenie o czytaniu referencji podczas renderowania.
2. Liczniki i wartości instancji, które nie wpływają na renderowanie
Tu useRef naprawdę błyszczy. Czas misji i identyfikator interwału to dane, które zmieniają się co sekundę, ale nie są wyświetlane na bieżąco:
1function MissionControl() {
2 const missionTimeRef = useRef(0);
3 const timerIdRef = useRef(null);
4
5 // Rozpoczęcie licznika misji
6 const startMissionTimer = () => {
7 if (timerIdRef.current !== null) return; // Unikamy wielokrotnego uruchamiania
8
9 timerIdRef.current = setInterval(() => {
10 missionTimeRef.current += 1;
11 console.log(`Czas misji: ${missionTimeRef.current} sekund`);
12 // Zauważ, że to NIE powoduje ponownego renderowania komponentu
13 }, 1000);
14 };
15
16 // Zatrzymanie licznika misji
17 const stopMissionTimer = () => {
18 clearInterval(timerIdRef.current);
19 timerIdRef.current = null;
20 };
21
22 // Czyszczenie przy odmontowaniu komponentu
23 useEffect(() => {
24 return () => {
25 if (timerIdRef.current !== null) {
26 clearInterval(timerIdRef.current);
27 }
28 };
29 }, []);
30
31 return (
32 <div>
33 <button onClick={startMissionTimer}>Rozpocznij misję</button>
34 <button onClick={stopMissionTimer}>Zakończ misję</button>
35 <button onClick={() => alert(`Czas misji: ${missionTimeRef.current} sekund`)}>
36 Pokaż czas misji
37 </button>
38 </div>
39 );
40}Licznik rośnie co sekundę, konsola to potwierdza, a komponent nie renderuje się ani razu. Referencje czytamy wyłącznie w funkcjach obsługi zdarzeń i w efekcie, czyli dokładnie tam, gdzie zaleca dokumentacja. Przyciski i ich wygląd pozostają niezmienione przez całą misję.
3. Dostęp do elementów DOM
Trzecie zastosowanie to uchwyt do prawdziwego elementu na stronie. Przekazujesz referencję w atrybucie ref, a React po wyrenderowaniu zapisze w .current węzeł DOM:
1function SpaceshipScanner() {
2 const scannerInputRef = useRef(null);
3
4 const focusScannerInput = () => {
5 // Bezpośredni dostęp do elementu DOM
6 scannerInputRef.current.focus();
7 };
8
9 return (
10 <div>
11 <input
12 ref={scannerInputRef}
13 type="text"
14 placeholder="Wprowadź współrzędne..."
15 />
16 <button onClick={focusScannerInput}>Aktywuj skaner</button>
17 </div>
18 );
19}Kliknięcie przycisku ustawia kursor w polu współrzędnych. React nie tworzy tu nowego pola, tylko daje ci dostęp do istniejącego. W React 19 możesz też przekazać ref jako zwykły prop do własnego komponentu funkcyjnego, bez forwardRef.
useRef a useCallback
Oba hooki coś "zapamiętują", ale co innego. useRef trzyma dowolną wartość, którą sam zmieniasz. useCallback zapamiętuje funkcję i tworzy nową dopiero wtedy, gdy zmieni się któraś z zależności:
1const handleClick = useCallback(() => {
2 onClick(id);
3}, [id, onClick]);Kolejność jest zawsze taka sama: funkcja, potem tablica zależności. Dopóki id i onClick się nie zmienią, handleClick pozostaje tym samym obiektem funkcji.
Pamiętaj o zasadach używania useRef
- Nie nadużywaj go: Używaj useRef tylko wtedy, gdy wartość naprawdę nie powinna powodować ponownego renderowania
- Unikaj bezpośrednich mutacji DOM: Chociaż useRef daje ci dostęp do elementów DOM, używaj go oszczędnie i zgodnie z filozofią Reacta
- Pamiętaj o czyszczeniu: Jeśli używasz useRef do przechowywania timerów lub subskrypcji, pamiętaj o ich czyszczeniu w funkcji czyszczącej useEffect
Moja rada: zanim sięgniesz po referencję, zapytaj, czy ta wartość ma być widoczna na ekranie. Jeśli tak, użyj stanu. Jeśli nie, referencja będzie lżejsza.
Podsumowanie
Hook useRef jest nieoceniony, gdy potrzebujesz:
- Przechować wartość między renderowaniami bez powodowania ponownego renderowania
- Śledzić poprzednie wartości
- Uzyskać bezpośredni dostęp do elementów DOM
- Przechowywać identyfikatory timerów lub inne wartości instancji
W następnej lekcji zbudujemy własne hooki, w których referencje często ukrywają szczegóły, na przykład timery.
Jak kapsuła ratunkowa w kosmosie, useRef przechowuje cenne dane, które mogą być potrzebne w krytycznych momentach misji twojego komponentu.
Kod do tej lekcji: App.jsx
1import React, { useState, useRef, useEffect } from 'react';
2
3// Śledzenie poprzednich wartości
4function PreviousValueTracker() {
5 const [position, setPosition] = useState({ x: 0, y: 0 });
6 const prevPositionRef = useRef({ x: 0, y: 0 });
7
8 useEffect(() => {
9 prevPositionRef.current = position;
10 });
11
12 const move = (dx, dy) => {
13 setPosition(prev => ({
14 x: Math.max(-100, Math.min(100, prev.x + dx)),
15 y: Math.max(-100, Math.min(100, prev.y + dy))
16 }));
17 };
18
19 return (
20 <div className="demo-card">
21 <h2>Sledzenie pozycji</h2>
22 <div className="position-display">
23 <div className="current">
24 <h3>Aktualna</h3>
25 <p>X: {position.x}</p>
26 <p>Y: {position.y}</p>
27 </div>
28 <div className="previous">
29 <h3>Poprzednia</h3>
30 <p>X: {prevPositionRef.current.x}</p>
31 <p>Y: {prevPositionRef.current.y}</p>
32 </div>
33 </div>
34 <div className="controls">
35 <button onClick={() => move(0, -10)}>UP</button>
36 <div className="horizontal">
37 <button onClick={() => move(-10, 0)}>LEFT</button>
38 <button onClick={() => move(10, 0)}>RIGHT</button>
39 </div>
40 <button onClick={() => move(0, 10)}>DOWN</button>
41 </div>
42 </div>
43 );
44}
45
46// Licznik renderow
47function RenderCounter() {
48 const renderCountRef = useRef(0);
49 const [value, setValue] = useState('');
50
51 renderCountRef.current++;
52
53 return (
54 <div className="demo-card">
55 <h2>Licznik renderow</h2>
56 <div className="render-count">
57 Rendery: <span>{renderCountRef.current}</span>
58 </div>
59 <input
60 value={value}
61 onChange={(e) => setValue(e.target.value)}
62 placeholder="Pisz, aby wywolac render..."
63 />
64 <p className="hint">
65 useRef przechowuje wartosc bez ponownego renderowania
66 </p>
67 </div>
68 );
69}
70
71// Porownanie obiektow
72function ObjectComparisonDemo() {
73 const [user, setUser] = useState({ name: 'Astronauta', level: 1 });
74 const prevUserRef = useRef(user);
75 const [changes, setChanges] = useState([]);
76
77 useEffect(() => {
78 const changedFields = [];
79
80 if (prevUserRef.current.name !== user.name) {
81 changedFields.push(`name: "${prevUserRef.current.name}" -> "${user.name}"`);
82 }
83 if (prevUserRef.current.level !== user.level) {
84 changedFields.push(`level: ${prevUserRef.current.level} -> ${user.level}`);
85 }
86
87 if (changedFields.length > 0) {
88 setChanges(prev => [...prev.slice(-4), ...changedFields]);
89 }
90
91 prevUserRef.current = user;
92 }, [user]);
93
94 return (
95 <div className="demo-card">
96 <h2>Porownanie zmian</h2>
97 <div className="user-info">
98 <input
99 value={user.name}
100 onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))}
101 placeholder="Imie"
102 />
103 <div className="level-controls">
104 <button onClick={() => setUser(prev => ({ ...prev, level: Math.max(1, prev.level - 1) }))}>
105 -
106 </button>
107 <span>Level: {user.level}</span>
108 <button onClick={() => setUser(prev => ({ ...prev, level: prev.level + 1 }))}>
109 +
110 </button>
111 </div>
112 </div>
113 <div className="changes">
114 <h4>Historia zmian:</h4>
115 {changes.map((change, i) => (
116 <div key={i} className="change">{change}</div>
117 ))}
118 </div>
119 </div>
120 );
121}
122
123// Cache wartosci obliczeniowych
124function ComputationCacheDemo() {
125 const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
126 const [multiplier, setMultiplier] = useState(2);
127 const cacheRef = useRef(new Map());
128 const cacheHits = useRef(0);
129 const cacheMisses = useRef(0);
130
131 const computeExpensive = (num, mult) => {
132 const key = `${num}-${mult}`;
133
134 if (cacheRef.current.has(key)) {
135 cacheHits.current += 1;
136 return cacheRef.current.get(key);
137 }
138
139 // Symulacja kosztownej operacji
140 const result = num * mult;
141 cacheRef.current.set(key, result);
142 cacheMisses.current += 1;
143 return result;
144 };
145
146 const results = numbers.map(n => computeExpensive(n, multiplier));
147
148 const addNumber = () => {
149 setNumbers(prev => [...prev, prev.length + 1]);
150 };
151
152 return (
153 <div className="demo-card">
154 <h2>Cache z useRef</h2>
155 <div className="cache-stats">
156 <span className="hit">Trafienia: {cacheHits.current}</span>
157 <span className="miss">Pudla: {cacheMisses.current}</span>
158 </div>
159 <div className="multiplier">
160 <button onClick={() => setMultiplier(m => Math.max(1, m - 1))}>-</button>
161 <span>Mnoznik: {multiplier}</span>
162 <button onClick={() => setMultiplier(m => m + 1)}>+</button>
163 </div>
164 <div className="numbers">
165 {numbers.map((n, i) => (
166 <div key={i} className="number-result">
167 {n} x {multiplier} = {results[i]}
168 </div>
169 ))}
170 </div>
171 <button onClick={addNumber}>+ Dodaj liczbe</button>
172 </div>
173 );
174}
175
176function App() {
177 return (
178 <div className="app">
179 <h1>Przechowywanie wartosci z useRef</h1>
180 <div className="demos">
181 <PreviousValueTracker />
182 <RenderCounter />
183 <ObjectComparisonDemo />
184 <ComputationCacheDemo />
185 </div>
186 </div>
187 );
188}
189
190export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż poprawną składnię useCallback dla handlera zdarzenia: