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.
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 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}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!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}ignore - alternatywa dla AbortControllerProstszy 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.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!