Sześć błędów synchronizacji czasu, które objawiają się jako błędy renderowania w React
Naucz się rozpoznawać sześć wzorców asynchronicznego JavaScript, od przestarzałych wartości przechwyconych do braku zwracanych obiektów Promise, które powodują, że komponenty React wyświetlają błędny lub niemożliwy stan.
Wiele problemów zarejestrowanych jako „błędy React” wcale nie pochodzi z samego Reacta. Lista pokazująca wyniki starszego zapytania, spinner, który nigdy nie znika, komunikat o sukcesie pojawiający się przed gotowością danych – React jest po prostu miejscem, gdzie te problemy stają się widoczne. Prawdziwa przyczyna zazwyczaj leży o poziom niżej, w sposobie, w jaki asynchroniczny JavaScript przekazuje wartości w czasie.
Kod asynchroniczny wprowadza element czasu do programu, a każde użycie await lub .then() stanowi moment, w którym sytuacja pod spodem może ulec zmianie. Poniżej przedstawiono sześć często występujących błędów związanych z czasem, powody, dla których każdy z nich powoduje mylące objawy w interfejsie, oraz niewielkie zmiany, które je naprawiają, aby można je było wykryć podczas przeglądania kodu.
Błąd 1: Przestarzałe żądania nadal zapisują dane do bieżącego stanu
Pole wyszukiwania to klasyczny przykład. Poniższy kod już teraz opóźnia wprowadzanie danych o 300 ms i usuwa timer podczas czyszczenia.
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const timeoutId = setTimeout(async () => {
const results = await search(query);
setResults(results);
}, 300);
return () => {
clearTimeout(timeoutId);
};
}, [query]);
Załóżmy teraz, że ktoś wpisuje „Async JavaScript Mistakes”. Wpisuje „Async”, czeka tyle czasu, ile potrzeba, by mechanizm debounce wygasł, a następnie wysyła żądanie. Potem kontynuuje wpisywanie tekstu, co powoduje wysłanie drugiego żądania zawierającego całą frazę. Mechanizm debounce zmniejszył liczbę żądań, ale nie zapobiegł temu, by dwa z nich były wysyłane jednocześnie.
Kolejność, w której żądania opuszczają przeglądarkę, jest pod twoją kontrolą. Kolejność, w której przychodzą odpowiedzi, już nie. Jeśli dłuższe zapytanie zostanie rozwiązane pierwsze, a zapytanie „Async” drugie, to druga wywołanie funkcji setResults zwycięży, w wyniku czego lista pokazuje wyniki dla „Async”, podczas gdy wpisany tekst faktycznie brzmi „Async JavaScript Mistakes”.
Nic tu nie działało niewłaściwie. Sieci mogą przestawiać kolejność dostarczanych odpowiedzi, a kod nigdy nie informował Reacta, która z nich jest nadal aktualna. Zwykłym rozwiązaniem jest anulowanie niepotrzebnych operacji za pomocą AbortController utworzonego wewnątrz funkcji effect i anulowanego podczas jej czyszczenia, dzięki czemu przestarzała odpowiedź albo w ogóle nie dotrze, albo zostanie zignorowana. Jeśli przepływ danych jest już oparty na RxJS, switchMap zapewnia tę samą semantykę „tylko najnowsze dane mają znaczenie”. Aby dowiedzieć się więcej na temat tego konkretnego scenariusza, zapoznaj się z rozwiązaniami problemów związanych z warunkami konkurencyjnymi, których nie da się rozwiązać za pomocą debouncingu.
Błąd 2: Podjęcie decyzji na podstawie wartości uzyskanych przed wywołaniem await
Traktuj każde await jako granicę. To, co wiedziałaś przed nim, to jedynie zrzut stanu; wszystko, co dzieje się po jego wykonaniu, ma miejsce w późniejszym momencie, być może po zmianie innego stanu aplikacji.
const handlePublish = async () => {
const { canPublish } = permissions;
await saveDraft();
if (canPublish) {
publish();
}
};
Ten obsługiwacz odczytuje wartość canPublish z pola permissions, czeka, aż projekt zostanie zapisany, a następnie decyduje, czy go opublikować. Problem polega na tym, że decyzja opiera się na wartości, która była prawdziwa na początku. Jeśli uprawnienia użytkownika zostały cofnięte podczas wykonywania funkcji saveDraft(), lokalna stała nadal wskazuje na możliwość publikacji.
Taki sam problem występuje w przypadku wybranych elementów, aktywnych filtrów, parametrów trasy, zawartości edytora oraz wielu innych elementów stanu aplikacji. Gdy decyzja po await zależy od aktualnego stanu aplikacji, należy ponownie odczytać ten stan po granicy (z refaktoryzowanego źródła, pamięci aplikacji lub nowej prośby), zamiast polegać na wcześniejszej kopii.
Błąd 3: Zakładanie, że reszta funkcji zawsze się wykona
Oto flaga ładowania umieszczona wokół wywołania fetch.
setLoading(true);
const dashboard = await getDashboard();
setDashboard(dashboard);
setLoading(false);
Jeśli getDashboard() zwróci błąd, wykonywanie przechodzi poza funkcję przy await, a setLoading(false) nigdy się nie uruchomi. Ikona spinowania pozostaje na ekranie w nieskończoność.
Gdy jakiś element stanu modeluje czas trwania operacji asynchronicznej, jego reset musi nastąpić zarówno w przypadku sukcesu, jak i niepowodzenia. Blok finally bezpośrednio wyraża tę intencję:
setLoading(true);
try {
const dashboard = await getDashboard();
setDashboard(dashboard);
} finally {
setLoading(false);
}
Zauważ, że blok try nadal nie zawiera bloku catch. Błąd nadal jest przekazywany osobie, która wywołała ten kod, co często jest pożądane, podczas gdy flaga ładowania zawsze zostanie wyłączona. Dodaj blok catch tylko wtedy, gdy to właściwe miejsce na przekształcenie błędu w element interfejsu, np. komunikat o błędzie.
Błąd 4: Niezależne żądania powodujące niespójne stany ekranu
Rozpoczynanie równolegle niepowiązanych ze sobą żądań jest często całkowicie uzasadnione.
getProfile().then(setProfile);
getPermissions().then(setPermissions);
getPreferences().then(setPreferences);
Każda odpowiedź trafia do odrębnego stanu w momencie swojego przybycia. Oznacza to, że React może wyświetlić dowolną kombinację: profil bez uprawnień ani ustawień, uprawnienia i ustawienia bez profilu itp., we wszystkich możliwych kolejnościach generowanych przez sieć.
Część z tych kombinacji może być bezsensowna lub nawet szkodliwa dla twojego ekranu. Gdy kilka odpowiedzi razem opisuje spójny stan ekranu, żądania mogą być realizowane równolegle, podczas gdy jeden właściciel komponuje wynik – na przykład poprzez oczekiwanie na Promise.all i zapisanie wszystkiego w jednej aktualizacji stanu, lub poprzez modelowanie ekranu jako reduktora z wyraźnymi stanami ładowania, gotowości i błędu. Równoległe pobieranie danych oraz niezależny stan interfejsu to dwie odrębne decyzje.
Błąd 5: Budowanie następnego stanu na podstawie przestarzałej kopii
Ten błąd łatwo się ukrywa w obsługiwaczach zdarzeń.
const handleAdd = async () => {
await saveItem(newItem);
setItems([...items, newItem]);
};
items to to, co komponent przechowywał w momencie rozpoczęcia wywołania handleAdd. Podczas gdy saveItem() było w trakcie realizacji, inna akcja mogła dodać lub usunąć elementy. Gdy obsługiwacz kontynuuje pracę, używa starego array i nadpisuje te nowsze zmiany.
Gdy kolejna wartość zależy od poprzedniej, niech React dostarczy tę poprzednią wartość za pośrednictwem formularza aktualizatora:
setItems(current => [...current, newItem]);
Aktualizator działa na podstawie najnowszego zapisanego stanu w momencie przetwarzania aktualizacji, dzięki czemu zachowują się jednoczesne zmiany. Stare zamknięcia nie są tylko problemem związanych efektów: każdy asynchroniczny callback może przechowywać wartości dłużej, niż się spodziewamy.
Błąd 6: Przerwanie łańcucha obietnic poprzez brak zwracania wartości
Ten łańcuch wygląda ściśle sekwencyjnie: zapisanie, następnie odświeżenie elementów interfejsu, potem oznaczenie ich jako zapisanych.
saveDashboard()
.then(() => refreshWidgets())
.then(() => setSaved(true));
Teraz spójrzmy, jak refreshWidgets może być napisany:
const refreshWidgets = () => {
getWidgets().then(setWidgets);
};
Funkcja inicjuje żądanie, ale zwraca undefined, a nie Promise. Z punktu widzenia zewnętrznej łańcuchowej struktury refreshWidgets() kończy się natychmiast, więc następny .then jest wykonywany od razu, a setSaved(true) może zostać uruchomiony, gdy elementy wciąż się ładują.
Rozwiązanie polega na użyciu jednego słowa kluczowego, aby zwrócić Promise, dzięki czemu łańcuch może na niego poczekać:
const refreshWidgets = () => {
return getWidgets().then(setWidgets);
};
Funkcja async osiąga ten sam efekt w sposób domyślny, ponieważ zawsze zwraca Promise, który zostaje sfinalizowany po zakończeniu jej treści:
const refreshWidgets = async () => {
const widgets = await getWidgets();
setWidgets(widgets);
};
W interfejsie użytkownika ten błąd może wyglądać jak zbyt wczesne pojawienie się komunikatu o sukcesie, dane przestarzałe pozostające na ekranie lub przekierowanie, które następuje przed zakończeniem odświeżenia. Żadne z tych objawów nie wskazuje oczywiście na brak instrukcji return. Zasady lintingu TypeScript, takie jak @typescript-eslint/no-floating-promises, mogą automatycznie wykryć wiele takich przypadków.
Główne wnioski
Różnica pomiędzy React a JavaScriptem nie zawsze jest oczywista. React renderuje stan, ale asynchroniczny JavaScript decyduje o tym, kiedy ten stan dotrze oraz czy w momencie jego przybycia jest nadal aktualny, przestarzały lub niespójny. Podczas analizy kodu asynchronicznego w komponentach trzy pytania pomagają zidentyfikować większość tych błędów:
- Kiedy została zapisana ta wartość i czy mogła ulec zmianie podczas wykonywania instrukcji
await?
Pozycje pokrewne
- Powszechne błędy zrozumienia async/await, które powodują błędy w produkcji — Wyjaśnia dziewięć subtelnych nieporozumień związanych z async/await – od warunków konkurencyjnych po nierozwiązane odrzucenia – które potajemnie psują aplikacje JavaScript w rzeczywistych warunkach.
- Naprawianie błędów w obróbce błędów Async/Await w kodzie produkcyjnym Node.js — Dowiedz się o pięciu powszechnych błędach w obróbce błędów async/await w JavaScript i Node.js, które powodują ciche awarie i sytuacje konkurencyjne, oraz o konkretnych sposobach ich naprawy.
- Localhost to nie środowisko produkcyjne: diagnozowanie aplikacji React, które zawodzą przy deployu — Dowiedz się, dlaczego aplikacja React, która działa poprawnie na twoim komputerze, zawodzi po deployu, oraz jak prześledzić URL-y API, zmienne środowiskowe, CORS, routowanie, pliki i mechanizmy autoryzacji aż do warstwy powodującej awarię.
- Snapshot czy wartość żywa? Określanie tego, co biorą opóźnione wywołania JavaScript — Dowiedz się, dlaczego zamknięcia zachowują dostęp do powiązań zamiast ich kopii, oraz jak wybierać między danymi ze snapshotu a danymi żywymi w timerach, efektach React, słuchaczach i kodzie asynchronicznym.
- Ukryte potoki zapytań: dopasowywanie kodu asynchronicznego do rzeczywistego grafu zależności — Naucz się rozpoznawać sekwencyjne wywołania awaits, które tworzą fałszywe zależności, naprawiać je za pomocą Promise.all oraz przekształcać panel sterowania React tak, aby każdy komponent pobierał tylko to, czego potrzebuje.
- Audyt useEffect: Ten powszechnych błędów, ich objawy i prawdziwe rozwiązania — Rozpoznaj dziesięć najczęstszych błędów w useEffect w React, od braku funkcji czyszczenia i przestarzałych zamykających funkcji po warunki wyścigowe i synchronizację propów, oraz zastosuj odpowiednie rozwiązanie.