W poprzednim module nauczyłeś się podstaw optymalizacji useEffect: właściwego dobierania zależności, używania flag
isCurrent do zapobiegania wyścigom i prostego debouncingu. Czas na zaawansowane techniki - jak prawdziwy oficer inżynieryjny na statku kosmicznym, musisz znać nie tylko podstawowe procedury, ale także protokoły awaryjne na wypadek najbardziej złożonych sytuacji.Prosta flaga
isCurrent ma wadę: request kontynuuje działanie w tle, zużywając zasoby sieciowe. Gdy użytkownik szybko przełącza się między stronami, w tle mogą działać dziesiątki bezużytecznych requestów. To jak wysyłanie sygnałów radiowych do stacji, z których już odleciałeś - marnujesz energię nadajnika.AbortController to wbudowany mechanizm przeglądarki, który pozwala faktycznie anulować request HTTP, a nie tylko ignorować jego wynik.1function PlanetDetails({ planetId }) {
2 const [planet, setPlanet] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 // Tworzymy kontroler do anulowania requestu
8 const abortController = new AbortController();
9
10 setLoading(true);
11 setError(null);
12
13 fetch(\`/api/planets/\${planetId}\`, {
14 signal: abortController.signal // Przekazujemy sygnał do fetch
15 })
16 .then(res => res.json())
17 .then(data => {
18 setPlanet(data);
19 setLoading(false);
20 })
21 .catch(err => {
22 // AbortError oznacza, że request został celowo anulowany
23 if (err.name !== 'AbortError') {
24 setError(err.message);
25 setLoading(false);
26 }
27 // Jeśli AbortError - nie robimy nic, bo komponent już nie istnieje
28 });
29
30 // Cleanup: anuluj request gdy planetId się zmieni lub komponent się odmontuje
31 return () => abortController.abort();
32 }, [planetId]);
33
34 if (loading) return <p>Ładowanie danych planety...</p>;
35 if (error) return <p>Błąd: {error}</p>;
36 return <h2>{planet?.name}</h2>;
37}Kluczowa różnica: z flagą
isCurrent request leci dalej w tle i zużywa transfer. Z AbortController przeglądarka naprawdę przerywa połączenie sieciowe. Przy dużych plikach lub wolnym połączeniu to ogromna oszczędność.Race condition (wyścig) w kontekście React to sytuacja, gdzie wynik asynchronicznej operacji trafia do komponentu w niewłaściwym momencie. Wyobraź sobie: astronauta klika na planetę Mars, potem szybko na Jowisza. Request o Marsa leci 2 sekundy, o Jowisza 0.5 sekundy. Jowisz wraca pierwszy - poprawnie wyświetlamy Jowisza. Ale po 2 sekundach wraca Mars i nadpisuje dane Jowisza!
1// PROBLEM: Race condition bez zabezpieczenia
2function StarMap({ starId }) {
3 const [star, setStar] = useState(null);
4
5 useEffect(() => {
6 // Każda zmiana starId uruchamia nowy fetch
7 // Ale poprzedni fetch nadal działa w tle!
8 fetchStarData(starId).then(data => {
9 setStar(data); // Może nadpisać nowsze dane!
10 });
11 }, [starId]);
12
13 return <div>{star?.name}</div>;
14}
15
16// ROZWIĄZANIE: AbortController eliminuje wyścig
17function StarMap({ starId }) {
18 const [star, setStar] = useState(null);
19
20 useEffect(() => {
21 const controller = new AbortController();
22
23 fetchStarData(starId, { signal: controller.signal })
24 .then(data => setStar(data))
25 .catch(err => {
26 if (err.name !== 'AbortError') console.error(err);
27 });
28
29 return () => controller.abort(); // Anuluj poprzedni request
30 }, [starId]);
31
32 return <div>{star?.name}</div>;
33}Stale closure (nieaktualne domknięcie) to subtelny bug, który pojawia się, gdy callback wewnątrz useEffect "widzi" starą wersję zmiennej stanu. To jak czytanie wczorajszego raportu sensorów, myśląc że to dane bieżące.
1// PROBLEM: Stale closure
2function AsteroidCounter() {
3 const [count, setCount] = useState(0);
4
5 useEffect(() => {
6 const timer = setInterval(() => {
7 // Ta funkcja "widzi" count z momentu utworzenia timera = zawsze 0!
8 setCount(count + 1); // Zawsze ustawia na 1, bo count = 0
9 }, 1000);
10
11 return () => clearInterval(timer);
12 }, []); // Pusta tablica = efekt uruchomiony raz, count zamknięty na 0
13
14 return <p>Asteroidy: {count}</p>;
15}
16
17// ROZWIĄZANIE 1: Użyj functional updater
18function AsteroidCounter() {
19 const [count, setCount] = useState(0);
20
21 useEffect(() => {
22 const timer = setInterval(() => {
23 // Functional updater zawsze otrzymuje aktualną wartość!
24 setCount(prevCount => prevCount + 1);
25 }, 1000);
26
27 return () => clearInterval(timer);
28 }, []);
29
30 return <p>Asteroidy: {count}</p>;
31}
32
33// ROZWIĄZANIE 2: Użyj useRef dla wartości, które muszą być aktualne
34function TrajectoryCalculator() {
35 const [velocity, setVelocity] = useState(100);
36 const velocityRef = useRef(velocity);
37
38 // Synchronizuj ref z najnowszą wartością
39 useEffect(() => {
40 velocityRef.current = velocity;
41 }, [velocity]);
42
43 useEffect(() => {
44 const timer = setInterval(() => {
45 // ref.current zawsze ma najnowszą wartość
46 console.log('Aktualna prędkość:', velocityRef.current);
47 }, 1000);
48
49 return () => clearInterval(timer);
50 }, []);
51
52 return <input value={velocity} onChange={e => setVelocity(Number(e.target.value))} />;
53}Reguła: Jeśli callback w
setInterval lub setTimeout potrzebuje aktualnej wartości stanu, użyj functional updater (setState(prev => ...)) lub useRef.Cleanup function w useEffect to nie tylko
clearInterval. W prawdziwych aplikacjach musisz czyścić wiele rodzajów zasobów jednocześnie.1function LiveDashboard({ stationId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 const controller = new AbortController();
6 let eventSource = null;
7 let reconnectTimer = null;
8
9 // 1. Fetch danych początkowych
10 fetch(\`/api/station/\${stationId}\`, { signal: controller.signal })
11 .then(res => res.json())
12 .then(setData)
13 .catch(err => {
14 if (err.name !== 'AbortError') console.error(err);
15 });
16
17 // 2. Server-Sent Events dla aktualizacji na żywo
18 function connectSSE() {
19 eventSource = new EventSource(\`/api/station/\${stationId}/live\`);
20 eventSource.onmessage = (event) => {
21 setData(JSON.parse(event.data));
22 };
23 eventSource.onerror = () => {
24 eventSource.close();
25 // Ponowne połączenie po 5 sekundach
26 reconnectTimer = setTimeout(connectSSE, 5000);
27 };
28 }
29 connectSSE();
30
31 // 3. Cleanup: zamknij WSZYSTKIE zasoby
32 return () => {
33 controller.abort(); // Anuluj fetch
34 if (eventSource) eventSource.close(); // Zamknij SSE
35 if (reconnectTimer) clearTimeout(reconnectTimer); // Anuluj reconnect
36 };
37 }, [stationId]);
38
39 return <div>{data?.status}</div>;
40}W poprzednim module poznałeś prosty debouncing. Teraz zobaczmy wzorzec, który poprawnie obsługuje cleanup i unika wycieków pamięci:
1function useDebounce(value, delay) {
2 const [debouncedValue, setDebouncedValue] = useState(value);
3
4 useEffect(() => {
5 // Ustaw timer na aktualizację zdebouncowanej wartości
6 const timer = setTimeout(() => {
7 setDebouncedValue(value);
8 }, delay);
9
10 // Cleanup: anuluj timer jeśli value zmieni się przed upływem delay
11 return () => clearTimeout(timer);
12 }, [value, delay]);
13
14 return debouncedValue;
15}
16
17// Użycie w komponencie wyszukiwarki
18function GalaxySearch() {
19 const [query, setQuery] = useState('');
20 const debouncedQuery = useDebounce(query, 400);
21 const [results, setResults] = useState([]);
22
23 // Ten efekt uruchamia się dopiero po 400ms od ostatniego znaku
24 useEffect(() => {
25 if (!debouncedQuery) {
26 setResults([]);
27 return;
28 }
29
30 const controller = new AbortController();
31
32 fetch(\`/api/search?q=\${debouncedQuery}\`, { signal: controller.signal })
33 .then(res => res.json())
34 .then(setResults)
35 .catch(err => {
36 if (err.name !== 'AbortError') console.error(err);
37 });
38
39 return () => controller.abort();
40 }, [debouncedQuery]);
41
42 return (
43 <div>
44 <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Szukaj w galaktyce..." />
45 <ul>
46 {results.map(r => <li key={r.id}>{r.name}</li>)}
47 </ul>
48 </div>
49 );
50}Zauważ, jak łączymy trzy wzorce: custom hook (
useDebounce), AbortController i cleanup. To typowy wzorzec w profesjonalnych aplikacjach React.Zaawansowane techniki zarządzania efektami:
setState(prev => ...)) lub useRef w callbackach timerów.