Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Graceful Degradation i Resilient UI

Na stacji kosmicznej awaria jednego systemu nie może oznaczac konca misji. Każdy krytyczny system ma kopie zapasowe, a zaloga jest przeszkolona do pracy w trybie awaryjnym. Twoją aplikacja React powinna dzialac tak samo - nawet jeśli cos pojdzie nie tak, użytkownik powinien moc kontynuowac prace.

Czym jest Graceful Degradation?

Graceful degradation (lagodna degradacja) to filozofia projektowania, w której aplikacja kontynuuje działanie nawet gdy niektore jej części zawiodą - po prostu z obnizonym poziomem funkcjonalności.

Pelna funkcjonalność vs. Degradacja

1// Zamiast: awaria całej strony
2function BrokenApp() {
3  return <WhiteScreenOfDeath />; // Użytkownik nie może nic zrobic
4}
5
6// Graceful degradation: części aplikacji nadal działają
7function ResilientApp() {
8  return (
9    <div>
10      <Navigation />                    {/* Zawsze działa */}
11      <ErrorBoundary fallback={<FallbackContent />}>
12        <MainContent />                 {/* Moze sie zepsuc */}
13      </ErrorBoundary>
14      <Footer />                        {/* Zawsze działa */}
15    </div>
16  );
17}

Wzorzec Retry (ponowna proba)

Gdy operacja sie nie powiedzie, daj użytkownikowi możliwość ponownej proby:

1import { ErrorBoundary } from 'react-error-boundary';
2
3function ErrorFallbackWithRetry({ error, resetErrorBoundary }) {
4  return (
5    <div className="error-panel">
6      <h3>Awaria modulu</h3>
7      <p>Błąd: {error.message}</p>
8      <button onClick={resetErrorBoundary}>
9        Ponowna proba
10      </button>
11    </div>
12  );
13}
14
15function App() {
16  return (
17    <ErrorBoundary
18      FallbackComponent={ErrorFallbackWithRetry}
19      onReset={() => {
20        // Wyczysc cache, zresetuj stan itp.
21      }}
22    >
23      <DataPanel />
24    </ErrorBoundary>
25  );
26}

Wzorzec Fallback Content

Zamiast pokazywac błąd, pokaz alternatywna treść:

1function WeatherWidget() {
2  const [data, setData] = useState(null);
3  const [error, setError] = useState(false);
4
5  useEffect(() => {
6    fetchWeather()
7      .then(setData)
8      .catch(() => setError(true));
9  }, []);
10
11  if (error) {
12    // Zamiast błędu, pokaz statyczne dane
13    return (
14      <div className="weather-widget offline">
15        <p>Dane pogodowe niedostepne</p>
16        <p>Ostatnia aktualizacja: 2h temu</p>
17        <small>Pracujemy nad przywroceniem połączenia</small>
18      </div>
19    );
20  }
21
22  if (!data) return <WeatherSkeleton />;
23  return <WeatherDisplay data={data} />;
24}

Partial Failure Handling

Obsluguj czesciowe awarie - gdy niektorych danych nie można pobrać:

1function MissionDashboard() {
2  const [missions, setMissions] = useState([]);
3  const [stats, setStats] = useState(null);
4  const [statsError, setStatsError] = useState(false);
5
6  useEffect(() => {
7    // Pobierz dane równoległe
8    Promise.allSettled([
9      fetch('/api/missions').then(r => r.json()),
10      fetch('/api/stats').then(r => r.json())
11    ]).then(([missionsResult, statsResult]) => {
12      if (missionsResult.status === 'fulfilled') {
13        setMissions(missionsResult.value);
14      }
15      if (statsResult.status === 'fulfilled') {
16        setStats(statsResult.value);
17      } else {
18        setStatsError(true);
19      }
20    });
21  }, []);
22
23  return (
24    <div>
25      <h2>Panel misji</h2>
26
27      {/* Statystyki - mogą byc niedostepne */}
28      {statsError ? (
29        <p className="warning">Statystyki chwilowo niedostepne</p>
30      ) : stats ? (
31        <StatsPanel data={stats} />
32      ) : (
33        <StatsSkeleton />
34      )}
35
36      {/* Lista misji - zawsze probujemy wyswietlic */}
37      <MissionList missions={missions} />
38    </div>
39  );
40}

Retry z exponential backoff

Dla krytycznych danych, implementuj automatyczne ponowne proby z rosnącymi opoznieniami:

1async function fetchWithRetry(url, maxRetries = 3) {
2  for (let attempt = 0; attempt < maxRetries; attempt++) {
3    try {
4      const response = await fetch(url);
5      if (!response.ok) throw new Error('Request failed');
6      return await response.json();
7    } catch (error) {
8      if (attempt === maxRetries - 1) throw error;
9      // Exponential backoff: 1s, 2s, 4s
10      const delay = Math.pow(2, attempt) * 1000;
11      await new Promise(resolve => setTimeout(resolve, delay));
12    }
13  }
14}
15
16function CriticalData() {
17  const [data, setData] = useState(null);
18  const [error, setError] = useState(null);
19  const [retrying, setRetrying] = useState(false);
20
21  const loadData = async () => {
22    setRetrying(true);
23    setError(null);
24    try {
25      const result = await fetchWithRetry('/api/critical-data');
26      setData(result);
27    } catch (err) {
28      setError(err);
29    } finally {
30      setRetrying(false);
31    }
32  };
33
34  useEffect(() => { loadData(); }, []);
35
36  if (error) return (
37    <div>
38      <p>Nie udalo sie pobrac danych po 3 probach</p>
39      <button onClick={loadData}>Sprobuj ponownie</button>
40    </div>
41  );
42  if (retrying) return <p>Ponawiam probe połączenia...</p>;
43  if (!data) return <LoadingSkeleton />;
44  return <DataDisplay data={data} />;
45}

Łączenie Error Boundary z Suspense

Najlepsza praktyka to łączenie obu mechanizmow:

1<ErrorBoundary FallbackComponent={ErrorUI}>
2  <Suspense fallback={<LoadingSkeleton />}>
3    <LazyComponent />
4  </Suspense>
5</ErrorBoundary>

Error Boundary przechwytuje błędy, a Suspense obsługuje ładowanie. Razem tworzą kompletny system bezpieczenstwa.

Ir a CodeWorlds