Slovenčina

Ovládnite React Suspense pre načítanie dát. Naučte sa deklaratívne riadiť načítanie, zlepšiť UX pomocou prechodov a spracovať chyby s Error Boundaries.

React Suspense Boundaries: Hĺbkový pohľad na deklaratívnu správu stavov načítania

Vo svete moderného webového vývoja je kľúčové vytvárať plynulý a responzívny používateľský zážitok. Jednou z najtrvalejších výziev, ktorým vývojári čelia, je správa stavov načítania. Od načítania dát pre používateľský profil až po načítanie novej sekcie aplikácie sú momenty čakania kritické. Historicky to zahŕňalo spletitú sieť booleovských príznakov ako isLoading, isFetching a hasError, roztrúsených po našich komponentoch. Tento imperatívny prístup zahlcuje náš kód, komplikuje logiku a je častým zdrojom chýb, ako sú race conditions (súbehy).

Prichádza React Suspense. Pôvodne bol predstavený pre rozdeľovanie kódu (code-splitting) s React.lazy(), no jeho schopnosti sa s React 18 dramaticky rozšírili a stal sa výkonným, prvotriednym mechanizmom na spracovanie asynchrónnych operácií, najmä načítania dát. Suspense nám umožňuje spravovať stavy načítania deklaratívnym spôsobom, čo zásadne mení spôsob, akým píšeme a uvažujeme o našich komponentoch. Namiesto otázky "Načítavam?", môžu naše komponenty jednoducho povedať: "Na vykreslenie potrebujem tieto dáta. Kým čakám, zobrazte prosím toto záložné UI."

Tento komplexný sprievodca vás prevedie cestou od tradičných metód správy stavu k deklaratívnej paradigme React Suspense. Preskúmame, čo sú Suspense boundaries, ako fungujú pre rozdeľovanie kódu aj načítanie dát a ako organizovať zložité používateľské rozhrania pre načítanie, ktoré vašich používateľov potešia, namiesto toho, aby ich frustrovali.

Starý spôsob: Drina s manuálnymi stavmi načítania

Predtým, ako si naplno uvedomíme eleganciu Suspense, je nevyhnutné pochopiť problém, ktorý rieši. Pozrime sa na typický komponent, ktorý načítava dáta pomocou hookov useEffect a useState.

Predstavte si komponent, ktorý potrebuje načítať a zobraziť dáta používateľa:


import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Resetovanie stavu pre nové userId
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Znovu načítať pri zmene userId

  if (isLoading) {
    return <p>Loading profile...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

Tento vzor je funkčný, ale má niekoľko nevýhod:

Vstupuje React Suspense: Zmena paradigmy

Suspense otáča tento model hore nohami. Namiesto toho, aby komponent spravoval stav načítania interne, komunikuje svoju závislosť na asynchrónnej operácii priamo Reactu. Ak dáta, ktoré potrebuje, ešte nie sú k dispozícii, komponent "pozastaví" (suspenduje) vykresľovanie.

Keď sa komponent pozastaví, React prechádza stromom komponentov smerom nahor, aby našiel najbližšiu Suspense Boundary (hranicu Suspense). Suspense Boundary je komponent, ktorý definujete vo svojom strome pomocou <Suspense>. Táto hranica potom vykreslí záložné UI (napríklad spinner alebo skeleton loader), kým všetky komponenty v nej nevyriešia svoje dátové závislosti.

Hlavnou myšlienkou je umiestniť dátovú závislosť spolu s komponentom, ktorý ju potrebuje, a zároveň centralizovať UI pre načítanie na vyššej úrovni v strome komponentov. Tým sa čistí logika komponentu a získavate silnú kontrolu nad používateľským zážitkom pri načítavaní.

Ako sa komponent "pozastaví"?

Kúzlo za Suspense spočíva vo vzore, ktorý sa na prvý pohľad môže zdať nezvyčajný: vyhodenie (throwing) Promise. Dátový zdroj s podporou Suspense funguje takto:

  1. Keď komponent požiada o dáta, dátový zdroj skontroluje, či má dáta v cache.
  2. Ak sú dáta k dispozícii, vráti ich synchrónne.
  3. Ak dáta nie sú k dispozícii (t.j. práve sa načítavajú), dátový zdroj vyhodí Promise, ktorý reprezentuje prebiehajúcu požiadavku na načítanie.

React tento vyhodený Promise zachytí. Nespôsobí to pád vašej aplikácie. Namiesto toho ho interpretuje ako signál: "Tento komponent ešte nie je pripravený na vykreslenie. Pozastav ho a hľadaj nad ním Suspense boundary, aby si zobrazil záložné UI." Keď sa Promise vyrieši (resolve), React sa pokúsi komponent znova vykresliť, ktorý teraz dostane svoje dáta a úspešne sa vykreslí.

Hranica <Suspense>: Váš deklarátor UI pre načítanie

Komponent <Suspense> je srdcom tohto vzoru. Jeho použitie je neuveriteľne jednoduché a prijíma jediný povinný prop: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>My Application</h1>
      <Suspense fallback={<p>Loading content...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

V tomto príklade, ak sa SomeComponentThatFetchesData pozastaví, používateľ uvidí správu "Loading content...", kým nebudú dáta pripravené. Fallback môže byť akýkoľvek platný React uzol, od jednoduchého reťazca po zložitý skeleton komponent.

Klasický prípad použitia: Rozdeľovanie kódu s React.lazy()

Najviac zavedeným použitím Suspense je rozdeľovanie kódu (code splitting). Umožňuje vám odložiť načítanie JavaScriptu pre komponent, až kým nie je skutočne potrebný.


import React, { Suspense, lazy } from 'react';

// Kód tohto komponentu nebude v počiatočnom balíku.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Some content that loads immediately</h2>
      <Suspense fallback={<div>Loading component...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Tu React načíta JavaScript pre HeavyComponent až vtedy, keď sa ho prvýkrát pokúsi vykresliť. Kým sa načítava a parsuje, zobrazí sa Suspense fallback. Toto je účinná technika na zlepšenie počiatočného času načítania stránky.

Moderná hranica: Načítanie dát so Suspense

Hoci React poskytuje mechanizmus Suspense, neposkytuje špecifického klienta na načítanie dát. Ak chcete použiť Suspense na načítanie dát, potrebujete dátový zdroj, ktorý sa s ním integruje (t.j. taký, ktorý vyhodí Promise, keď sú dáta v stave pending).

Frameworky ako Relay a Next.js majú vstavanú, prvotriednu podporu pre Suspense. Populárne knižnice na načítanie dát ako TanStack Query (predtým React Query) a SWR tiež ponúkajú experimentálnu alebo plnú podporu.

Aby sme pochopili koncept, vytvorme si veľmi jednoduchý, koncepčný obal okolo fetch API, aby bol kompatibilný so Suspense. Poznámka: Toto je zjednodušený príklad na vzdelávacie účely a nie je pripravený na produkčné použitie. Chýba mu správne cachovanie a zložitejšie spracovanie chýb.


// data-fetcher.js
// Jednoduchá cache na ukladanie výsledkov
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Toto je to kúzlo!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Fetch failed with status ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Tento obal udržiava jednoduchý stav pre každú URL. Keď sa zavolá fetchData, skontroluje stav. Ak je `pending`, vyhodí promise. Ak je úspešný, vráti dáta. Teraz si prepíšme náš komponent UserProfile s použitím tohto mechanizmu.


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// Komponent, ktorý skutočne používa dáta
function ProfileDetails({ userId }) {
  // Pokúsi sa prečítať dáta. Ak nie sú pripravené, pozastaví sa.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Rodičovský komponent, ktorý definuje UI pre stav načítania
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Loading profile...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Pozrite sa na ten rozdiel! Komponent ProfileDetails je čistý a zameraný výlučne na vykreslenie dát. Nemá žiadne stavy isLoading ani error. Jednoducho si vyžiada dáta, ktoré potrebuje. Zodpovednosť za zobrazenie indikátora načítania sa presunula na rodičovský komponent, UserProfile, ktorý deklaratívne určuje, čo sa má zobraziť počas čakania.

Organizácia zložitejších stavov načítania

Skutočná sila Suspense sa prejaví, keď vytvárate zložité UI s viacerými asynchrónnymi závislosťami.

Vnorené Suspense Boundaries pre postupné UI

Môžete vnárať Suspense boundaries, aby ste vytvorili prepracovanejší zážitok z načítania. Predstavte si stránku s dashboardom, ktorá má bočný panel, hlavnú obsahovú oblasť a zoznam posledných aktivít. Každá z týchto častí môže vyžadovať vlastné načítanie dát.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <div className="layout">
        <Suspense fallback={<p>Loading navigation...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

S touto štruktúrou:

To vám umožňuje zobraziť používateľovi užitočný obsah čo najrýchlejšie, čím sa dramaticky zlepšuje vnímaný výkon.

Predchádzanie "Popcorningu" v UI

Niekedy môže postupný prístup viesť k rušivému efektu, keď sa viacero spinnerov objaví a zmizne v rýchlom slede za sebou, efekt často nazývaný "popcorning". Aby ste to vyriešili, môžete presunúť Suspense boundary vyššie v strome.


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

V tejto verzii sa zobrazí jeden DashboardSkeleton, kým všetky vnorené komponenty (Sidebar, MainContent, ActivityFeed) nemajú pripravené svoje dáta. Celý dashboard sa potom objaví naraz. Voľba medzi vnorenými hranicami a jednou hranicou na vyššej úrovni je rozhodnutím dizajnu UX, ktoré Suspense umožňuje triviálne implementovať.

Spracovanie chýb s Error Boundaries

Suspense spracúva stav pending (čakajúci) promise, ale čo stav rejected (zamietnutý)? Ak sa promise vyhodený komponentom zamietne (napr. chyba siete), bude sa s ním zaobchádzať ako s akoukoľvek inou chybou pri vykresľovaní v Reacte.

Riešením je použiť Error Boundaries (hranice chýb). Error Boundary je triedny komponent, ktorý definuje špeciálnu metódu životného cyklu, componentDidCatch() alebo statickú metódu getDerivedStateFromError(). Zachytáva JavaScriptové chyby kdekoľvek vo svojom strome vnorených komponentov, zaznamenáva tieto chyby a zobrazuje záložné UI.

Tu je jednoduchý komponent Error Boundary:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Aktualizuje stav, aby ďalšie vykreslenie zobrazilo záložné UI.
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // Chybu môžete tiež zaznamenať do služby na hlásenie chýb
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Môžete vykresliť akékoľvek vlastné záložné UI
      return <h1>Something went wrong. Please try again.</h1>;
    }

    return this.props.children; 
  }
}

Potom môžete kombinovať Error Boundaries so Suspense a vytvoriť robustný systém, ktorý spracuje všetky tri stavy: čakajúci, úspešný a chybný.


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>User Information</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Loading...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

S týmto vzorom, ak je načítanie dát vnútri UserProfile úspešné, zobrazí sa profil. Ak je v stave `pending`, zobrazí sa fallback Suspense. Ak zlyhá, zobrazí sa fallback z Error Boundary. Logika je deklaratívna, kompozitná a ľahko pochopiteľná.

Prechody (Transitions): Kľúč k neblokujúcim aktualizáciám UI

Existuje ešte posledný kúsok skladačky. Zvážte interakciu používateľa, ktorá spustí nové načítanie dát, ako je kliknutie na tlačidlo "Ďalší" na zobrazenie iného profilu používateľa. S vyššie uvedeným nastavením, v momente kliknutia na tlačidlo a zmeny prop userId, sa komponent UserProfile opäť pozastaví. To znamená, že aktuálne viditeľný profil zmizne a nahradí ho záložné UI pre načítavanie. To môže pôsobiť náhle a rušivo.

Tu prichádzajú na scénu prechody (transitions). Prechody sú novou funkciou v React 18, ktorá vám umožňuje označiť určité aktualizácie stavu ako neurgentné. Keď je aktualizácia stavu zabalená do prechodu, React bude naďalej zobrazovať staré UI (zastaraný obsah), zatiaľ čo pripravuje nový obsah na pozadí. Aktualizáciu UI vykoná až vtedy, keď je nový obsah pripravený na zobrazenie.

Primárnym API pre toto je hook useTransition.


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Next User
      </button>

      {isPending && <span> Loading new profile...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Loading initial profile...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Teraz sa stane toto:

  1. Načíta sa počiatočný profil pre userId: 1, zobrazí sa fallback Suspense.
  2. Používateľ klikne na "Next User" (Ďalší používateľ).
  3. Volanie setUserId je zabalené v startTransition.
  4. React začne v pamäti vykresľovať UserProfile s novým userId 2. To spôsobí jeho pozastavenie.
  5. Kľúčové je, že namiesto zobrazenia fallbacku Suspense, React ponechá na obrazovke staré UI (profil používateľa 1).
  6. Booleovská hodnota isPending vrátená z useTransition sa stane true, čo nám umožňuje zobraziť jemný, inline indikátor načítania bez odpojenia (unmounting) starého obsahu.
  7. Akonáhle sú dáta pre používateľa 2 načítané a UserProfile sa môže úspešne vykresliť, React vykoná aktualizáciu a nový profil sa plynule objaví.

Prechody poskytujú poslednú vrstvu kontroly, ktorá vám umožňuje vytvárať sofistikované a používateľsky prívetivé zážitky z načítania, ktoré nikdy nepôsobia rušivo.

Najlepšie postupy a globálne úvahy

Záver

React Suspense predstavuje viac než len novú funkciu; je to fundamentálna evolúcia v tom, ako pristupujeme k asynchrónnosti v React aplikáciách. Prechodom od manuálnych, imperatívnych príznakov načítania k deklaratívnemu modelu môžeme písať komponenty, ktoré sú čistejšie, odolnejšie a ľahšie sa skladajú.

Kombináciou <Suspense> pre stavy čakania, Error Boundaries pre chybové stavy a useTransition pre plynulé aktualizácie máte k dispozícii kompletný a výkonný súbor nástrojov. Môžete organizovať všetko od jednoduchých spinnerov až po zložité, postupné odhaľovanie dashboardov s minimálnym, predvídateľným kódom. Keď začnete integrovať Suspense do svojich projektov, zistíte, že nielen zlepšuje výkon a používateľský zážitok vašej aplikácie, ale tiež dramaticky zjednodušuje logiku správy stavu, čo vám umožní sústrediť sa na to, na čom skutočne záleží: na vytváranie skvelých funkcií.