Granice błędów
W przeszłości błędy javascriptowe wewnątrz komponentów uszkadzały wewnętrzny stan Reacta i wywoływały tajemnicze błędy w kolejnych renderowaniach. Były one następstwem wcześniejszego błędu w kodzie aplikacji, jednakże React nie dostarczał żadnego rozwiązania pozwalającego na właściwe ich obsłużenie wewnątrz komponentów oraz nie potrafił odtworzyć aplikacji po ich wystąpieniu.
Przedstawiamy granice błędów
Błąd w kodzie JavaScript, występujący w jednej z części interfejsu użytkownika (UI), nie powinien psuć całej aplikacji. Aby rozwiązać ten problem, w Reakcie 16 wprowadziliśmy koncepcję granic błędów (ang. error boundary).
Granice błędów to komponenty reactowe, które przechwytują błędy javascriptowe występujące gdziekolwiek wewnątrz drzewa komponentów ich potomków, a następnie logują je i wyświetlają zastępczy interfejs UI, zamiast pokazywać ten niepoprawnie działający. Granice błędów przechwytują błędy występujące podczas renderowania, w metodach cyklu życia komponentów, a także w konstruktorach całego podrzędnego im drzewa komponentów.
Uwaga
Granice błędów nie obsługują błędów w:
- Procedurach obsługi zdarzeń (ang. event handlers) (informacje)
- Asynchronicznym kodzie (np. w metodach:
setTimeout
lub w funkcjach zwrotnychrequestAnimationFrame
)- Komponentach renderowanych po stronie serwera
- Błędach rzuconych w samych granicach błędów (a nie w ich podrzędnych komponentach)
Aby komponent klasowy stał się granicą błędu, musi definiować jedną lub obie metody cyklu życia: static getDerivedStateFromError()
i/lub componentDidCatch()
. Należy używać static getDerivedStateFromError()
do wyrenderowania zastępczego UI po rzuceniu błędu, a componentDidCatch()
, aby zalogować informacje o błędzie.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) { // Zaktualizuj stan, aby następny render pokazał zastępcze UI. return { hasError: true }; }
componentDidCatch(error, errorInfo) { // Możesz także zalogować błąd do zewnętrznego serwisu raportowania błędów logErrorToMyService(error, errorInfo); }
render() {
if (this.state.hasError) { // Możesz wyrenderować dowolny interfejs zastępczy. return <h1>Something went wrong.</h1>; }
return this.props.children;
}
}
Po zdefiniowaniu, granicy błędu można używać jak normalnego komponentu:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
Granice błędów działają jak bloki catch {}
w JavaScript, tyle że dla komponentów. Można je zdefiniować tylko w komponentach klasowych. W praktyce, w większości przypadków wystarczy zdefiniować jeden komponent granicy błędu i używać go w całej aplikacji.
Należy pamiętać, że granice błędów wyłapują błędy w komponentach potomnych. Nie są one jednak w stanie obsłużyć własnych błędów. W takim przypadku, jeżeli granica błędu nie będzie w stanie wyświetlić zastępczego UI, błąd zostanie przekazany do kolejnej najbliższej granicy błędu powyżej w strukturze komponentów. Jest to zachowanie podobne do tego znanego z javascriptowych bloków catch {}
.
Demo
Przykład tworzenia i użycia granicy błędów.
Gdzie umiejscowić granice błędów
Poziom granularności granic błędów zależy wyłącznie od ciebie. Możesz opakować tylko główny komponent aplikacji i wyświetlać tekst “Coś poszło nie tak”, jak to zwykle robi się we frameworkach serwerowycha. Możesz też otoczyć każdy z widgetów aplikacji osobną granicą błędów, aby błędy w ich wnętrzu nie zabijały całej aplikacji.
Nowe zachowanie nieobsłużonych błędów
Wprowadzenie granic błędów ma ważne następstwo. Od Reacta w wersji 16, błędy, które nie zostały obsłużone za pomocą granicy błędów, spowodują odmontowanie całego drzewa komponentów.
Przedyskutowaliśmy tę zmianę i z naszego doświadczenia wynika, że lepiej jest usunąć całe drzewo komponentów niż wyświetlać zepsute fragmenty UI. Na przykład, w produkcie takim jak Messenger pozostawienie wyświetlonego zepsutego kawałka UI może sprawić, że ktoś nieświadomie wyśle wiadomość do innej osoby. Również w aplikacjach finansowych wyświetlanie złego stanu konta jest gorszą sytuacją niż nie wyświetlenie niczego.
Ta zmiana oznacza, że wraz z migracją do nowej wersji Reacta odkryte zostaną błędy w aplikacjach, które do tej pory nie zostały zauważone. Dodanie granic błędów zapewni lepsze doświadczenie dla użytkownika, gdy coś pójdzie nie tak.
Przykładowo, Facebook Messenger opakowuje w osobne granice błędów następujące fragmenty aplikacji: zawartość paska bocznego, panel z informacjami o konwersacji, listę konwersacji i pole tekstowe na wiadomość. Jeżeli jeden z tych elementów zadziała nieprawidłowo, reszta pozostanie interaktywa i działająca.
Zachęcamy również do używania (lub zbudowania własnego) narzędzia do raportowania błędów, dzięki czemu będzie możliwe poznanie nieobsłużonych błędów występujących w środowisku produkcyjnym.
Ślad stosu komponentów
React 16, w środowisku deweloperskim, wyświetla w konsoli wszystkie błędy złapane podczas renderowania, nawet jeżeli aplikacja przypadkowo je przejmie. Oprócz wiadomości błędu i javascriptowego stosu, dostępny jest również stos komponentów. Dzięki temu wiadomo, gdzie dokładnie w drzewie komponentów wystąpił błąd:
W drzewie komponentów widoczne są również numery linii i nazwy plików. Ten mechanizm domyślnie działa w aplikacjach stworzonych przy użyciu Create React App:
Jeżeli nie używasz Create React App, możesz ręcznie dodać ten plugin do swojej konfiguracji Babela. Został on stworzony do używania tylko podczas fazy deweloperskiej i powinien zostać wyłączony w środowisku produkcyjnym
Uwaga
Nazwy komponentów wyświetlane w śladzie stosu zależą od własności
Function.name
. Jeżeli obsługujesz starsze przeglądarki, które nie dostarczają jej natywnie (np. IE 11), możesz dodać łatkę taką jakfunction.name-polyfill
. Alternatywą jest zadeklarowanie wprostdisplayName
we wszystkich komponentach.
A co z try/catch?
try
/ catch
jest świetnym rozwiązaniem, ale działa tylko dla imperatywnego kodu:
try {
showButton();
} catch (error) {
// ...
}
Natomiast komponenty reactowe są deklaratywne i określają, co powinno zostać wyrenderowane:
<Button />
Granice błędów zachowują deklaratywną naturę Reacta. Na przykład, jeżeli w metodzie componentDidUpdate
wystąpi błąd podczas aktualizacji stanu, aplikacja poprawnie przekaże błąd do najbliższej granicy błędów.
A co z procedurami obsługi zdarzeń?
Granice błędów nie obsługują błędów z procedur obsługi zdarzeń.
React nie potrzebuje granic błędów do przywrócenia aplikacji po błędzie powstałych w procedurze obsługi zdarzeń. W przeciwieństwie do metod cyklu życia komponentu lub metody renderującej, procedury obsługi zdarzeń nie są wywoływane w trakcie renderowania. Dzięki temu nawet w przypadku błędu React wie, co wyświetlić na ekranie.
Aby obsłużyć błąd w procedurze obsługi zdarzenia, należy użyć javascriptowego try
/ catch
:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
try { // Kod, który rzuci wyjątek } catch (error) { this.setState({ error }); } }
render() {
if (this.state.error) { return <h1>Caught an error.</h1> } return <button onClick={this.handleClick}>Click Me</button> }
}
Powyższy przykład prezentuje normalne zachowanie JavaScriptu i nie używa granic błędów.
Zmiany nazewnictwa od Reacta w wersji 15
React 15 zawierał bardzo okrojoną obsługę granic błędów za pomocą metody o nazwie unstable_handleError
. Ta metoda nie jest już obsługiwana i należy zmienić jej nazwę na componentDidCatch
począwszy od pierwszych beta wersji Reacta 16.
Ze względu na tę zmianę stworzyliśmy codemod, który automatycznie przekształci twój kod.