Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Zaawansowane efekty — race conditions i debouncing

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.

AbortController — anulowanie requestów

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 conditions — głębsza analiza

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 closures — pułapka nieaktualnych wartości

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
.

Zaawansowane wzorce cleanup

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}

Debounce z cleanup — prawidłowy wzorzec

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.

Podsumowanie

Zaawansowane techniki zarządzania efektami:

  1. AbortController - anuluj requesty zamiast ignorować odpowiedzi. Oszczędza transfer i zasoby.
  2. Race conditions - używaj AbortController lub flag cancel, aby zapobiec nadpisaniu nowszych danych starszymi.
  3. Stale closures - używaj functional updater (
    setState(prev => ...)
    ) lub
    useRef
    w callbackach timerów.
  4. Złożony cleanup - zamykaj wszystkie zasoby: fetch, SSE, timery, event listenery.
  5. Debounce + AbortController - łącz oba wzorce dla optymalnego wyszukiwania.
Ir a CodeWorlds