Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Race Conditions i AbortController

Wyobraź sobie, że Twoj statek kosmiczny wysyła zapytania do trzech różnych stacji kosmicznych jednoczesnie. Pierwsza odpowiada po 5 sekundach, druga po 2, trzecia po 8. Jeśli nie kontrolujesz kolejnosci odpowiedzi, możesz wyswietlic dane z nieaktualnego zapytania! To jest race condition -- jeden z najczestszych i najtrudniejszych do wychwycenia bledow w aplikacjach React.

Czym jest Race Condition?

Race condition w data fetchingu występuje, gdy wynik starszego zapytania nadpisuje wynik nowszego:

1// PROBLEM: Race Condition!
2function PlanetInfo({ planetId }) {
3  const [planet, setPlanet] = useState(null);
4
5  useEffect(() => {
6    // Uzytkownik klika szybko: planetId = 1, potem 2, potem 3
7    // Zapytanie 1 wysylane (trwa 3s)
8    // Zapytanie 2 wysylane (trwa 1s) <- odpowie PIERWSZE!
9    // Zapytanie 3 wysylane (trwa 2s)
10    // Kolejnosc odpowiedzi: 2, 3, 1
11    // Wynik: wyswietlamy planete 1, bo jej odpowiedz przyszla OSTATNIA!
12
13    async function load() {
14      const res = await fetch(
15        \`https://swapi.dev/api/planets/\${planetId}/\`
16      );
17      const data = await res.json();
18      setPlanet(data); // Kazda odpowiedz nadpisuje stan!
19    }
20    load();
21  }, [planetId]);
22
23  return <div>{planet?.name}</div>;
24}

AbortController - anulowanie zapytan

AbortController
to wbudowany mechanizm przegladarki do anulowania zapytan HTTP. Działa jak przycisk "Anuluj transmisje" na panelu komunikacyjnym:

1function PlanetInfo({ planetId }) {
2  const [planet, setPlanet] = useState(null);
3  const [loading, setLoading] = useState(true);
4
5  useEffect(() => {
6    // Tworzymy kontroler dla TEGO efektu
7    const controller = new AbortController();
8
9    async function loadPlanet() {
10      try {
11        setLoading(true);
12        const response = await fetch(
13          \`https://swapi.dev/api/planets/\${planetId}/\`,
14          { signal: controller.signal } // Podlaczamy sygnal
15        );
16        const data = await response.json();
17        setPlanet(data);
18      } catch (err) {
19        if (err.name === 'AbortError') {
20          // To normalne anulowanie - ignorujemy
21          console.log('Zapytanie anulowane');
22        } else {
23          console.error('Blad:', err);
24        }
25      } finally {
26        setLoading(false);
27      }
28    }
29
30    loadPlanet();
31
32    // Cleanup: anuluj zapytanie gdy:
33    // 1. planetId sie zmieni (nowy efekt)
34    // 2. komponent sie odmontuje
35    return () => controller.abort();
36  }, [planetId]);
37
38  return <div>{loading ? 'Ladowanie...' : planet?.name}</div>;
39}

Jak cleanup zapobiega race conditions

Mechanizm jest prosty ale potezny:

1// Uzytkownik klika: planeta 1 -> planeta 2 -> planeta 3
2
3// Krok 1: useEffect uruchamia sie z planetId=1
4//   -> tworzy controller_1, wysyla fetch_1
5
6// Krok 2: planetId zmienia sie na 2
7//   -> React wywoluje cleanup z Kroku 1: controller_1.abort()
8//   -> fetch_1 zostaje ANULOWANY (AbortError)
9//   -> useEffect uruchamia sie z planetId=2
10//   -> tworzy controller_2, wysyla fetch_2
11
12// Krok 3: planetId zmienia sie na 3
13//   -> React wywoluje cleanup z Kroku 2: controller_2.abort()
14//   -> fetch_2 zostaje ANULOWANY
15//   -> useEffect uruchamia sie z planetId=3
16//   -> tworzy controller_3, wysyla fetch_3
17
18// Wynik: tylko fetch_3 sie zakonczy i zaktualizuje stan!

Stale Closures - przestan blad domkniec

Inny podstepny problem to stale closure -- funkcja "pamięta" stara wartosc zmiennej:

1// PROBLEM: stale closure
2function Counter() {
3  const [count, setCount] = useState(0);
4
5  useEffect(() => {
6    const interval = setInterval(() => {
7      // Ta funkcja "zamknela" count = 0
8      // Zawsze widzi 0, nawet po wielu renderach!
9      setCount(count + 1); // Zawsze ustawia 1!
10    }, 1000);
11
12    return () => clearInterval(interval);
13  }, []); // Pusta tablica = closure z pierwszego rendera
14
15  return <div>{count}</div>;
16}
17
18// ROZWIAZANIE: uzywaj callback w setState
19function Counter() {
20  const [count, setCount] = useState(0);
21
22  useEffect(() => {
23    const interval = setInterval(() => {
24      // Callback dostaje AKTUALNA wartosc prev
25      setCount(prev => prev + 1); // Zawsze poprawne!
26    }, 1000);
27
28    return () => clearInterval(interval);
29  }, []);
30
31  return <div>{count}</div>;
32}

Flaga
ignore
- alternatywa dla AbortController

Prostszy wzorzec zapobiegania race conditions - flaga boolean:

1function SearchResults({ query }) {
2  const [results, setResults] = useState([]);
3
4  useEffect(() => {
5    let ignore = false; // Flaga: czy wynik jest aktualny?
6
7    async function search() {
8      const response = await fetch(
9        \`https://swapi.dev/api/people/?search=\${query}\`
10      );
11      const data = await response.json();
12
13      // Sprawdz flage PRZED aktualizacja stanu
14      if (!ignore) {
15        setResults(data.results);
16      }
17    }
18
19    search();
20
21    // Cleanup: oznacz wynik jako nieaktualny
22    return () => {
23      ignore = true;
24    };
25  }, [query]);
26
27  return (
28    <ul>
29      {results.map(r => <li key={r.name}>{r.name}</li>)}
30    </ul>
31  );
32}

Flaga

ignore
nie anuluje zapytania (nadal zuzywa zasoby sieciowe), ale zapobiega aktualizacji stanu nieaktualnymi danymi.

Wzorzec: bezpieczny fetch z pelna ochrona

Polaczmy wszystkie techniki w jeden bezpieczny wzorzec:

1function SafeDataFetcher({ endpoint }) {
2  const [data, setData] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    const controller = new AbortController();
8
9    async function fetchData() {
10      try {
11        setLoading(true);
12        setError(null);
13
14        const response = await fetch(endpoint, {
15          signal: controller.signal,
16        });
17
18        if (!response.ok) {
19          throw new Error(\`HTTP \${response.status}\`);
20        }
21
22        const result = await response.json();
23        // Jesli dotarlismy tutaj, zapytanie nie zostalo anulowane
24        setData(result);
25      } catch (err) {
26        if (err.name !== 'AbortError') {
27          setError(err.message);
28        }
29        // AbortError = normalne anulowanie, nie ustawiamy bledu
30      } finally {
31        if (!controller.signal.aborted) {
32          setLoading(false);
33        }
34      }
35    }
36
37    fetchData();
38    return () => controller.abort();
39  }, [endpoint]);
40
41  return { data, loading, error };
42}

Race conditions to cichy wrog kazdego kosmicznego nawigatora. Bez AbortControllera i prawidlowego cleanup, Twoje dane moga byc z niewlasciwej galaktyki!

Ir a CodeWorlds