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 zwrotnych requestAnimationFrame)
  • 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 z wykorzystaniem Reacta 16.

Gdzie umiejscowić granice błędów

To, jak bardzo szczegółowo zostanie pokryty kod za pomocą granic błędów, jest kwestią preferencji. Możliwe jest, na przykład, opakowanie granicą błędów komponentu najwyższego poziomu odpowiedzialnego za routing aplikacji, aby wyświetlić informację: “Coś poszło nie tak” - tak jak ma to często miejsce w frameworkach po stronie serwera. Można również opakować pojedyncze fragmenty aplikacji, aby uchronić jej pozostałe części przed błędami.

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:

Błąd złapany w komponencie będącym granicą błędów

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:

Błąd złapany w komponencie będącym granicą błędów wraz z numerami linii

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ą jak function.name-polyfill. Alternatywą jest zadeklarowanie wprost displayName 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 <div onClick={this.handleClick}>Click Me</div>
  }
}

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.