Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense

react-error-boundary - nowoczesna biblioteka

5 min czytania
W tej lekcji6

Twoja klasowa grodź z poprzedniej lekcji działa ale spójrz, czego jej brakuje: nie da się jej zresetować po naprawie modułu, nie zareaguje na błąd z kliknięcia ani z fetch, a każdy projekt przepisuje ją od nowa. Tworzenie Error Boundaries od zera wymaga pisania komponentów klasowych, co w dobie hooków może wydawać się archaiczne. Na szczęście istnieje biblioteka react-error-boundary, która znacznie upraszcza pracę z obsługą błędów. Poleca ją nawet dokumentacja react.dev jako gotową alternatywę dla własnej klasy.

Instalacja i podstawowe użycie

Bibliotekę instalujesz jak każdą paczkę z npm. Poniższe polecenie dotyczy npm, w projekcie z yarn wpiszesz yarn add react-error-boundary:

1npm install react-error-boundary

W chwili pisania aktualna jest wersja 6.x. Działa z React 19 (oraz 18), jest dystrybuowana jako moduł ES i jest komponentem klienckim, więc w Next.js App Router plik z nią musi mieć dyrektywę "use client".

Biblioteka udostępnia gotowy komponent ErrorBoundary z wieloma przydatnymi funkcjami. Najpierw definiujemy komponent zastępczy, potem owijamy nim stację i podpinamy dwa callbacki:

1import { ErrorBoundary } from 'react-error-boundary';
2
3function ErrorFallback({ error, resetErrorBoundary }) {
4  return (
5    <div className="error-panel">
6      <h2>Awaria modułu!</h2>
7      <p>{error.message}</p>
8      <button onClick={resetErrorBoundary}>
9        Restart modułu
10      </button>
11    </div>
12  );
13}
14
15function App() {
16  return (
17    <ErrorBoundary
18      FallbackComponent={ErrorFallback}
19      onError={(error, info) => {
20        // Logowanie do serwisu monitoringu
21        console.error('Logged error:', error);
22      }}
23      onReset={() => {
24        // Reset stanu aplikacji po naprawie błędu
25        console.log('Error boundary reset');
26      }}
27    >
28      <SpaceStation />
29    </ErrorBoundary>
30  );
31}

onError odpowiada metodzie componentDidCatch: dostaje błąd i obiekt z componentStack. onReset wywoła się, gdy granica zostanie wyczyszczona, na przykład po kliknięciu "Restart modułu". Wtedy React spróbuje ponownie wyrenderować SpaceStation. Jedna uwaga do wersji 6: w typach TypeScript error ma typ unknown, bo rzucić można cokolwiek, nie tylko obiekt Error. Jeśli chcesz bezpiecznie wyciągnąć komunikat, biblioteka eksportuje pomocniczą funkcję getErrorMessage(error).

FallbackComponent vs fallback vs fallbackRender

Biblioteka oferuje trzy sposoby definiowania zastępczego UI. Robią to samo, różnią się wygodą i dostępem do danych o błędzie.

1. FallbackComponent (zalecane)

Przekazujesz komponent, a biblioteka sama wyrenderuje go z propsami error i resetErrorBoundary:

1function MyFallback({ error, resetErrorBoundary }) {
2  return (
3    <div>
4      <p>Błąd: {error.message}</p>
5      <button onClick={resetErrorBoundary}>Spróbuj ponownie</button>
6    </div>
7  );
8}
9
10<ErrorBoundary FallbackComponent={MyFallback}>
11  <App />
12</ErrorBoundary>

Taki komponent można przetestować osobno i użyć w wielu granicach, dlatego to mój domyślny wybór.

2. fallback (prosty JSX)

Gdy nie potrzebujesz treści błędu ani przycisku resetu, wystarczy gotowy element JSX:

1<ErrorBoundary fallback={<p>Coś poszło nie tak!</p>}>
2  <App />
3</ErrorBoundary>

Ten wariant nie dostaje żadnych propsów, więc załoga zobaczy komunikat, ale nie zrestartuje modułu.

3. fallbackRender (render prop)

Trzecia opcja to funkcja wywoływana z tymi samymi danymi, wygodna do krótkich, jednorazowych fallbacków:

1<ErrorBoundary
2  fallbackRender={({ error, resetErrorBoundary }) => (
3    <div>
4      <p>{error.message}</p>
5      <button onClick={resetErrorBoundary}>Reset</button>
6    </div>
7  )}
8>
9  <App />
10</ErrorBoundary>

Funkcja otrzymuje ten sam obiekt co FallbackComponent, zmienia się tylko forma zapisu.

Hook useErrorBoundary

Jedno z najpotężniejszych narzędzi biblioteki - hook useErrorBoundary pozwala programowo wywoływać błędy w Error Boundary z poziomu event handlerów i kodu asynchronicznego! Zwraca funkcję showBoundary(error), która przekazuje błąd do najbliższej granicy, oraz resetBoundary(), która ją czyści:

1import { useErrorBoundary } from 'react-error-boundary';
2
3function DataFetcher() {
4  const { showBoundary } = useErrorBoundary();
5  const [data, setData] = useState(null);
6
7  const fetchData = async () => {
8    try {
9      const response = await fetch('/api/missions');
10      if (!response.ok) {
11        throw new Error('Błąd pobierania danych misji');
12      }
13      const result = await response.json();
14      setData(result);
15    } catch (error) {
16      // Przekaż błąd do najbliższego Error Boundary!
17      showBoundary(error);
18    }
19  };
20
21  return (
22    <div>
23      <button onClick={fetchData}>Pobierz dane misji</button>
24      {data && <p>Misje: {data.length}</p>}
25    </div>
26  );
27}

Sam try/catch nadal łapie błąd, zmienia się tylko to, co z nim robimy: zamiast lokalnego komunikatu uruchamiamy ten sam fallback, który obsługuje błędy renderowania. W React 19 istnieje też alternatywa: błąd rzucony w funkcji przekazanej do startTransition z useTransition również trafia do najbliższej granicy.

resetKeys - automatyczny reset

Możesz skonfigurować resetKeys - listę wartości, których zmiana automatycznie zresetuje Error Boundary. To przydatne, gdy awaria zależy od wybranych danych:

1function App() {
2  const [selectedPlanet, setSelectedPlanet] = useState('Mars');
3
4  return (
5    <ErrorBoundary
6      FallbackComponent={ErrorFallback}
7      resetKeys={[selectedPlanet]}
8      onReset={() => {
9        // Czyszczenie po resecie
10        console.log('Boundary reset - nowa planeta:', selectedPlanet);
11      }}
12    >
13      <PlanetDetails planet={selectedPlanet} />
14    </ErrorBoundary>
15  );
16}
17// Gdy użytkownik zmieni planetę, Error Boundary automatycznie się zresetuje

Warto wiedzieć, że po zmianie klucza onReset dostaje obiekt z polem reason: 'keys', a przy resecie przyciskiem reason: 'imperative-api'. Dzięki temu wiesz, co wywołało restart.

Nested Error Boundaries

Możesz zagnieżdżać wiele Error Boundaries dla różnych poziomów obsługi, tak jak w poprzedniej lekcji:

1<ErrorBoundary FallbackComponent={AppCrashScreen}>
2  <Header />
3  <ErrorBoundary FallbackComponent={SectionError}>
4    <MissionControl />
5  </ErrorBoundary>
6  <ErrorBoundary FallbackComponent={SectionError}>
7    <CrewPanel />
8  </ErrorBoundary>
9</ErrorBoundary>

Jeśli błąd wystąpi w MissionControl, zostanie przechwycony przez wewnętrzny ErrorBoundary. Jeśli wewnętrzny sam rzuci błąd, przechwycony zostanie przez zewnętrzny.

Porównanie: własny Error Boundary vs react-error-boundary

Zestawmy obie grodzie obok siebie:

CechaWłasny (klasowy)react-error-boundary
ResetRęcznie implementowanyWbudowany (resetErrorBoundary)
Auto-resetBrakresetKeys
HookBrakuseErrorBoundary
LoggingRęcznieWbudowany (onError)
Async errorsNie przechwytujeshowBoundary()
BoilerplateDużoMinimum

Kluczowa zaleta biblioteki to hook useErrorBoundary - pozwala przechwytywać błędy z event handlerów i kodu asynchronicznego, czego natywne Error Boundaries nie potrafią. W praktyce, jeśli budujesz poważną aplikację produkcyjną, react-error-boundary jest standardem branżowym - tak jak ustandaryzowane systemy bezpieczeństwa na statkach kosmicznych. Pod spodem to wciąż ta sama klasa z getDerivedStateFromError, więc wszystko, czego nauczyłeś się wcześniej, nadal obowiązuje. W kolejnych lekcjach połączysz ją z formularzami, monitoringiem i Suspense.

Pamiętaj: nie buduj grodzi od zera na każdym statku, sięgnij po sprawdzony moduł i skup się na tym, co zobaczy załoga.

Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Symulacja react-error-boundary
5// (w produkcji: import { ErrorBoundary } from 'react-error-boundary')
6
7class ErrorBoundary extends React.Component {
8  constructor(props) {
9    super(props);
10    this.state = { hasError: false, error: null };
11  }
12
13  static getDerivedStateFromError(error) {
14    return { hasError: true, error };
15  }
16
17  componentDidCatch(error, info) {
18    if (this.props.onError) {
19      this.props.onError(error, info);
20    }
21  }
22
23  resetErrorBoundary = () => {
24    if (this.props.onReset) {
25      this.props.onReset();
26    }
27    this.setState({ hasError: false, error: null });
28  };
29
30  render() {
31    if (this.state.hasError) {
32      const { FallbackComponent, fallback, fallbackRender } = this.props;
33      if (FallbackComponent) {
34        return (
35          <FallbackComponent
36            error={this.state.error}
37            resetErrorBoundary={this.resetErrorBoundary}
38          />
39        );
40      }
41      if (fallbackRender) {
42        return fallbackRender({
43          error: this.state.error,
44          resetErrorBoundary: this.resetErrorBoundary,
45        });
46      }
47      if (fallback) return fallback;
48      return <p>Cos poszlo nie tak</p>;
49    }
50    return this.props.children;
51  }
52}
53
54// FallbackComponent
55function ModuleError({ error, resetErrorBoundary }) {
56  return (
57    <div className="error-card">
58      <h3>Awaria modulu</h3>
59      <p className="error-msg">{error.message}</p>
60      <button onClick={resetErrorBoundary} className="retry-btn">
61        Restart modulu
62      </button>
63    </div>
64  );
65}
66
67// Niestabilny komponent
68function UnstablePanel({ name }) {
69  if (Math.random() < 0.5) {
70    throw new Error(name + ': krytyczny blad systemu!');
71  }
72  return (
73    <div className="panel-ok">
74      <span className="dot green" />
75      {name} - Online
76    </div>
77  );
78}
79
80function StablePanel({ name }) {
81  return (
82    <div className="panel-ok">
83      <span className="dot green" />
84      {name} - Online
85    </div>
86  );
87}
88
89export default function App() {
90  const [key, setKey] = useState(0);
91
92  return (
93    <div className="app">
94      <h1>react-error-boundary</h1>
95      <p className="subtitle">
96        Trzy sposoby definiowania fallback UI
97      </p>
98      <button onClick={() => setKey(k => k + 1)} className="reset-all">
99        Zresetuj wszystko
100      </button>
101
102      <div className="panels" key={key}>
103        <div className="panel-section">
104          <h2>FallbackComponent</h2>
105          <ErrorBoundary
106            FallbackComponent={ModuleError}
107            onError={(err) => console.log('Logged:', err.message)}
108            onReset={() => console.log('Reset!')}
109          >
110            <UnstablePanel name="Nawigacja" />
111          </ErrorBoundary>
112        </div>
113
114        <div className="panel-section">
115          <h2>fallbackRender</h2>
116          <ErrorBoundary
117            fallbackRender={({ error, resetErrorBoundary }) => (
118              <div className="error-card">
119                <p>{error.message}</p>
120                <button onClick={resetErrorBoundary} className="retry-btn">
121                  Napraw
122                </button>
123              </div>
124            )}
125          >
126            <UnstablePanel name="Komunikacja" />
127          </ErrorBoundary>
128        </div>
129
130        <div className="panel-section">
131          <h2>fallback (prosty JSX)</h2>
132          <ErrorBoundary fallback={<p className="simple-err">Modul offline</p>}>
133            <UnstablePanel name="Sensory" />
134          </ErrorBoundary>
135        </div>
136
137        <div className="panel-section">
138          <h2>Stabilny modul</h2>
139          <ErrorBoundary FallbackComponent={ModuleError}>
140            <StablePanel name="Podtrzymywanie zycia" />
141          </ErrorBoundary>
142        </div>
143      </div>
144    </div>
145  );
146}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jakie propsy otrzymuje FallbackComponent w react-error-boundary?

  2. 2. Do czego służy hook useErrorBoundary z biblioteki react-error-boundary?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż poprawną składnię użycia ErrorBoundary z FallbackComponent:

  • Edytor kodu

    Zaimplementuj async error handling

  • Klikanie w kolejności

    Kliknij elementy w kolejności składni ErrorBoundary z resetowaniem:

  • Klikanie w kolejności

    Kliknij elementy w kolejności użycia useErrorBoundary z fetch:

Przydatne artykuły