Slovenčina

Preskúmajte React Suspense na načítavanie dát mimo delenia kódu. Pochopte Fetch-As-You-Render, spracovanie chýb a nadčasové vzory pre globálne aplikácie.

Načítavanie zdrojov s React Suspense: Zvládnutie moderných vzorov načítavania dát

V dynamickom svete webového vývoja je používateľský zážitok (UX) prvoradý. Očakáva sa, že aplikácie budú rýchle, responzívne a príjemné na používanie, bez ohľadu na podmienky siete alebo schopnosti zariadenia. Pre vývojárov v Reacte to často znamená zložitú správu stavov, komplexné indikátory načítavania a neustály boj proti vodopádom pri načítavaní dát. Prichádza React Suspense, silná, hoci často nepochopená, funkcia navrhnutá tak, aby zásadne zmenila spôsob, akým narábame s asynchrónnymi operáciami, najmä s načítavaním dát.

Pôvodne predstavený pre delenie kódu s React.lazy(), skutočný potenciál Suspense spočíva v jeho schopnosti organizovať načítavanie *akéhokoľvek* asynchrónneho zdroja, vrátane dát z API. Tento komplexný sprievodca sa ponorí hlboko do React Suspense pre načítavanie zdrojov, preskúma jeho základné koncepty, fundamentálne vzory načítavania dát a praktické úvahy pre budovanie výkonných a odolných globálnych aplikácií.

Evolúcia načítavania dát v Reacte: Od imperatívneho k deklaratívnemu

Po mnoho rokov sa načítavanie dát v komponentoch Reactu primárne spoliehalo na bežný vzor: použitie hooku useEffect na spustenie volania API, správa stavov načítavania a chýb pomocou useState a podmienené renderovanie na základe týchto stavov. Hoci tento prístup bol funkčný, často viedol k niekoľkým problémom:

Zvážte typický scenár načítavania dát bez Suspense:

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(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Načítava sa profil používateľa...</p>;
  }

  if (error) {
    return <p style={"color: red;"}>Chyba: {error.message}</p>;
  }

  if (!user) {
    return <p>Nie sú dostupné žiadne údaje o používateľovi.</p>;
  }

  return (
    <div>
      <h2>Používateľ: {user.name}</h2>
      <p>Email: {user.email}</p>
      <!-- Ďalšie detaily o používateľovi -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Vitajte v aplikácii</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

Tento vzor je všadeprítomný, ale núti komponent spravovať svoj vlastný asynchrónny stav, čo často vedie k tesne prepojenému vzťahu medzi UI a logikou načítavania dát. Suspense ponúka deklaratívnejšiu a zjednodušenú alternatívu.

Pochopenie React Suspense mimo delenia kódu

Väčšina vývojárov sa so Suspense prvýkrát stretne prostredníctvom React.lazy() pre delenie kódu, kde umožňuje odložiť načítanie kódu komponentu, kým nie je potrebný. Napríklad:

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

const LazyComponent = lazy(() => import('./MyHeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Načítava sa komponent...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

V tomto scenári, ak MyHeavyComponent ešte nebol načítaný, hranica <Suspense> zachytí promise (sľub) vyvolaný funkciou lazy() a zobrazí fallback, kým kód komponentu nebude pripravený. Kľúčovým poznatkom je, že Suspense funguje tak, že zachytáva promises (sľuby) vyvolané počas renderovania.

Tento mechanizmus nie je exkluzívny len pre načítavanie kódu. Akákoľvek funkcia volaná počas renderovania, ktorá vyvolá promise (napr. pretože zdroj ešte nie je dostupný), môže byť zachytená hranicou Suspense vyššie v strome komponentov. Keď sa promise vyrieši, React sa pokúsi znova renderovať komponent, a ak je zdroj teraz dostupný, fallback sa skryje a zobrazí sa skutočný obsah.

Základné koncepty Suspense pre načítavanie dát

Aby sme mohli využiť Suspense na načítavanie dát, musíme porozumieť niekoľkým základným princípom:

1. Vyvolanie Promise (sľubu)

Na rozdiel od tradičného asynchrónneho kódu, ktorý používa async/await na riešenie promises, Suspense sa spolieha na funkciu, ktorá *vyvolá* promise, ak dáta nie sú pripravené. Keď sa React pokúsi renderovať komponent, ktorý volá takúto funkciu, a dáta sú stále čakajúce, promise je vyvolaný. React potom 'pozastaví' renderovanie tohto komponentu a jeho detí, hľadajúc najbližšiu hranicu <Suspense>.

2. Hranica Suspense

Komponent <Suspense> funguje ako hranica chyby pre promises. Prijíma prop fallback, čo je UI, ktoré sa má renderovať, zatiaľ čo niektoré z jeho detí (alebo ich potomkov) sú v stave pozastavenia (t.j. vyvolávajú promise). Akonáhle sa všetky promises vyvolané v jeho podstrome vyriešia, fallback je nahradený skutočným obsahom.

Jedna hranica Suspense môže spravovať viacero asynchrónnych operácií. Napríklad, ak máte dva komponenty v rámci tej istej hranice <Suspense> a každý potrebuje načítať dáta, fallback sa zobrazí, kým nebudú dokončené *obidva* procesy načítania dát. Tým sa zabráni zobrazovaniu čiastočného UI a poskytne sa koordinovanejší zážitok z načítavania.

3. Správca keše/zdrojov (zodpovednosť vývojára)

Je dôležité si uvedomiť, že Suspense sám o sebe nespravuje načítavanie dát ani kešovanie. Je to len koordinačný mechanizmus. Aby Suspense fungoval na načítavanie dát, potrebujete vrstvu, ktorá:

Tento 'správca zdrojov' je typicky implementovaný pomocou jednoduchej keše (napr. Map alebo objekt) na ukladanie stavu každého zdroja (čakajúci, vyriešený alebo chybný). Hoci si to môžete vytvoriť manuálne na demonštračné účely, v reálnej aplikácii by ste použili robustnú knižnicu na načítavanie dát, ktorá sa integruje so Suspense.

4. Concurrent Mode (vylepšenia v React 18)

Hoci Suspense sa dá použiť aj v starších verziách Reactu, jeho plný potenciál sa uvoľní s Concurrent Reactom (štandardne povolený v React 18 s createRoot). Concurrent Mode umožňuje Reactu prerušiť, pozastaviť a obnoviť prácu na renderovaní. To znamená:

Vzory načítavania dát so Suspense

Preskúmajme evolúciu vzorov načítavania dát s príchodom Suspense.

Vzor 1: Fetch-Then-Render (tradičný prístup s obalením v Suspense)

Toto je klasický prístup, kde sa dáta načítajú a až potom sa komponent renderuje. Hoci priamo nevyužíva mechanizmus 'vyvolania promise' pre dáta, môžete obaliť komponent, ktorý *nakoniec* renderuje dáta, do hranice Suspense, aby ste poskytli fallback. Ide skôr o použitie Suspense ako generického orchestrátora UI načítavania pre komponenty, ktoré sa nakoniec stanú pripravenými, aj keď ich interné načítavanie dát je stále založené na tradičnom useEffect.

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

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

  useEffect(() => {
    const fetchUserData = async () => {
      setIsLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setIsLoading(false);
    };
    fetchUserData();
  }, [userId]);

  if (isLoading) {
    return <p>Načítavajú sa detaily používateľa...</p>;
  }

  return (
    <div>
      <h3>Používateľ: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Príklad Fetch-Then-Render</h1>
      <Suspense fallback={<div>Celkové načítavanie stránky...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

Výhody: Jednoduché na pochopenie, spätne kompatibilné. Môže sa použiť ako rýchly spôsob pridania globálneho stavu načítavania.

Nevýhody: Neeliminuje boilerplate kód vnútri UserDetails. Stále náchylné na vodopády, ak komponenty načítavajú dáta sekvenčne. Skutočne nevyužíva mechanizmus 'vyvolaj-a-chyť' Suspense pre samotné dáta.

Vzor 2: Render-Then-Fetch (načítavanie vnútri renderovania, nie pre produkciu)

Tento vzor slúži primárne na ilustráciu toho, čo nerobiť so Suspense priamo, pretože to môže viesť k nekonečným cyklom alebo problémom s výkonom, ak sa to nerieši dôkladne. Zahŕňa pokus o načítanie dát alebo volanie pozastavujúcej funkcie priamo vo fáze renderovania komponentu, *bez* správneho mechanizmu kešovania.

// NEPOUŽÍVAJTE V PRODUKCII BEZ SPRÁVNEJ VRSTVY KEŠOVANIA
// Toto je čisto na ilustráciu toho, ako by priame 'vyvolanie' mohlo koncepčne fungovať.

let fetchedData = null;
let dataPromise = null;

function fetchDataSynchronously(url) {
  if (fetchedData) {
    return fetchedData;
  }

  if (!dataPromise) {
    dataPromise = fetch(url)
      .then(res => res.json())
      .then(data => { fetchedData = data; dataPromise = null; return data; })
      .catch(err => { dataPromise = null; throw err; });
  }
  throw dataPromise; // Tu nastupuje Suspense
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>Používateľ: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Render-Then-Fetch (ilustračné, NEODPORÚČA SA priamo)</h1>
      <Suspense fallback={<div>Načítava sa používateľ...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

Výhody: Ukazuje, ako môže komponent priamo 'požiadať' o dáta a pozastaviť sa, ak nie sú pripravené.

Nevýhody: Veľmi problematické pre produkciu. Tento manuálny, globálny systém fetchedData a dataPromise je zjednodušený, nerieši robustne viacero požiadaviek, invalidáciu ani chybové stavy. Je to primitívna ilustrácia konceptu 'vyvolania promise', nie vzor, ktorý by sa mal osvojiť.

Vzor 3: Fetch-As-You-Render (ideálny vzor pre Suspense)

Toto je zmena paradigmy, ktorú Suspense skutočne umožňuje pre načítavanie dát. Namiesto čakania na renderovanie komponentu pred načítaním jeho dát, alebo načítania všetkých dát vopred, Fetch-As-You-Render znamená, že začnete načítavať dáta *čo najskôr*, často *pred* alebo *súbežne s* procesom renderovania. Komponenty potom 'čítajú' dáta z keše, a ak dáta nie sú pripravené, pozastavia sa. Základnou myšlienkou je oddeliť logiku načítavania dát od logiky renderovania komponentu.

Na implementáciu Fetch-As-You-Render potrebujete mechanizmus na:

  1. Iniciovanie načítania dát mimo renderovacej funkcie komponentu (napr. pri vstupe na routu alebo po kliknutí na tlačidlo).
  2. Uloženie promise alebo vyriešených dát do keše.
  3. Poskytnutie spôsobu, akým môžu komponenty 'čítať' z tejto keše. Ak dáta ešte nie sú dostupné, funkcia čítania vyvolá čakajúci promise.

Tento vzor rieši problém vodopádu. Ak dva rôzne komponenty potrebujú dáta, ich požiadavky môžu byť iniciované paralelne a UI sa zobrazí až vtedy, keď budú *obidva* pripravené, čo je koordinované jednou hranicou Suspense.

Manuálna implementácia (pre pochopenie)

Aby sme pochopili základné mechanizmy, vytvorme si zjednodušeného manuálneho správcu zdrojov. V reálnej aplikácii by ste použili dedikovanú knižnicu.

import React, { Suspense } from 'react';

// --- Jednoduchý správca keše/zdrojov --- //
const cache = new Map();

function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

function fetchData(key, fetcher) {
  if (!cache.has(key)) {
    cache.set(key, createResource(fetcher()));
  }
  return cache.get(key);
}

// --- Funkcie na načítavanie dát --- //
const fetchUserById = (id) => {
  console.log(`Načítava sa používateľ ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
      '2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
      '3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`Načítavajú sa príspevky pre používateľa ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'Môj prvý príspevok' }, { id: 'p2', title: 'Cestovateľské dobrodružstvá' }],
      '2': [{ id: 'p3', title: 'Postrehy z kódovania' }],
      '3': [{ id: 'p4', title: 'Globálne trendy' }, { id: 'p5', title: 'Miestna kuchyňa' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Komponenty --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // Toto spôsobí suspendovanie, ak údaje používateľa nie sú pripravené

  return (
    <div>
      <h3>Používateľ: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // Toto spôsobí suspendovanie, ak údaje príspevkov nie sú pripravené

  return (
    <div>
      <h4>Príspevky od {userId}:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>Nenašli sa žiadne príspevky.</li>}
      </ul>
    </div>
  );
}

// --- Aplikácia --- //
let initialUserResource = null;
let initialPostsResource = null;

function prefetchDataForUser(userId) {
  initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}

// Prednačítanie niektorých dát ešte pred renderovaním komponentu App
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Fetch-As-You-Render so Suspense</h1>
      <p>Toto demonštruje, ako môže načítavanie dát prebiehať paralelne, koordinované pomocou Suspense.</p>

      <Suspense fallback={<div>Načítava sa profil používateľa a príspevky...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Ďalšia sekcia</h2>
      <Suspense fallback={<div>Načítava sa iný používateľ...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

V tomto príklade:

Knižnice pre Fetch-As-You-Render

Vytváranie a údržba robustného správcu zdrojov je zložité. Našťastie, niekoľko zrelých knižníc na načítavanie dát prijalo alebo prijíma Suspense, poskytujúc osvedčené riešenia:

Tieto knižnice abstrahujú zložitosť vytvárania a správy zdrojov, riešia kešovanie, revalidáciu, optimistické aktualizácie a spracovanie chýb, čo značne zjednodušuje implementáciu Fetch-As-You-Render.

Vzor 4: Prednačítavanie s knižnicami podporujúcimi Suspense

Prednačítavanie je silná optimalizácia, pri ktorej proaktívne načítavate dáta, ktoré bude používateľ pravdepodobne v blízkej budúcnosti potrebovať, ešte predtým, ako ich explicitne požiada. To môže drasticky zlepšiť vnímaný výkon.

S knižnicami podporujúcimi Suspense sa prednačítavanie stáva bezproblémovým. Môžete spustiť načítavanie dát pri interakciách používateľa, ktoré okamžite nemenia UI, ako je napríklad prejdenie myšou nad odkazom alebo tlačidlom.

import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// Predpokladajme, že toto sú vaše volania API
const fetchProductById = async (id) => {
  console.log(`Načítava sa produkt ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'Všestranný widget pre medzinárodné použitie.' },
      'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Špičkový gadget, obľúbený po celom svete.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Štandardne povoliť Suspense pre všetky dopyty
    },
  },
});

function ProductDetails({ productId }) {
  const { data: product } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProductById(productId),
  });

  return (
    <div style={{"border": "1px solid #ccc", "padding": "15px", "margin": "10px 0"}}>
      <h3>{product.name}</h3>
      <p>Cena: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // Prednačítať dáta, keď používateľ prejde myšou ponad odkaz na produkt
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Prednačítava sa produkt ${productId}`);
  };

  return (
    <div>
      <h2>Dostupné produkty:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Prejdite alebo zobrazte detaily */ }}
          >Global Widget X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Prejdite alebo zobrazte detaily */ }}
          >Universal Gadget Y (B002)</a>
        </li>
      </ul>
      <p>Prejdite myšou ponad odkaz na produkt, aby ste videli prednačítavanie v akcii. Pre sledovanie otvorte záložku siete.</p>
    </div>
  );
}

function App() {
  const [showProductA, setShowProductA] = React.useState(false);
  const [showProductB, setShowProductB] = React.useState(false);

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Prednačítavanie s React Suspense (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Zobraziť Global Widget X</button>
      <button onClick={() => setShowProductB(true)}>Zobraziť Universal Gadget Y</button>

      {showProductA && (
        <Suspense fallback={<p>Načítava sa Global Widget X...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Načítava sa Universal Gadget Y...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

V tomto príklade spustí prejdenie myšou nad odkazom na produkt `queryClient.prefetchQuery`, čo iniciuje načítanie dát na pozadí. Ak používateľ potom klikne na tlačidlo na zobrazenie detailov produktu a dáta sú už v keši z prednačítania, komponent sa renderuje okamžite bez pozastavenia. Ak prednačítanie stále prebieha alebo nebolo iniciované, Suspense zobrazí fallback, kým dáta nebudú pripravené.

Spracovanie chýb so Suspense a Error Boundaries

Zatiaľ čo Suspense spracováva stav 'načítavania' zobrazením fallbacku, priamo nespracováva 'chybové' stavy. Ak je promise vyvolaný pozastavujúcim komponentom zamietnutý (t.j. načítavanie dát zlyhá), táto chyba sa bude šíriť hore stromom komponentov. Na elegantné spracovanie týchto chýb a zobrazenie vhodného UI musíte použiť Error Boundaries (hranice chýb).

Error Boundary je React komponent, ktorý implementuje buď životné cykly componentDidCatch alebo static getDerivedStateFromError. Zachytáva JavaScriptové chyby kdekoľvek vo svojom podstromu komponentov, vrátane chýb vyvolaných promises, ktoré by Suspense normálne zachytil, keby boli čakajúce.

import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// --- Komponent Error Boundary --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Aktualizuje stav, aby nasledujúce renderovanie zobrazilo záložné UI.
    return { hasError: true, error };
  }

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

  render() {
    if (this.state.hasError) {
      // Môžete renderovať akékoľvek vlastné záložné UI
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Niečo sa pokazilo!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Skúste obnoviť stránku alebo kontaktujte podporu.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Skúsiť znova</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Načítavanie dát (s potenciálom chyby) --- //
const fetchItemById = async (id) => {
  console.log(`Pokus o načítanie položky ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Nepodarilo sa načítať položku: Sieť je nedostupná alebo položka nebola nájdená.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Doručené pomaly', data: 'Táto položka trvala dlhšie, ale dorazila!', status: 'success' });
    } else {
      resolve({ id, name: `Položka ${id}`, data: `Dáta pre položku ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // Pre demonštráciu, vypnite opakovanie, aby bola chyba okamžitá
    },
  },
});

function DisplayItem({ itemId }) {
  const { data: item } = useQuery({
    queryKey: ['item', itemId],
    queryFn: () => fetchItemById(itemId),
  });

  return (
    <div>
      <h3>Detaily položky:</h3>
      <p>ID: {item.id}</p>
      <p>Meno: {item.name}</p>
      <p>Dáta: {item.data}</p>
    </div>
  );
}

function App() {
  const [fetchType, setFetchType] = useState('normal-item');

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense a Error Boundaries</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Načítať normálnu položku</button>
        <button onClick={() => setFetchType('slow-item')}>Načítať pomalú položku</button>
        <button onClick={() => setFetchType('error-item')}>Načítať chybnú položku</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Načítava sa položka cez Suspense...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

Obalením vašej hranice Suspense (alebo komponentov, ktoré sa môžu pozastaviť) do Error Boundary zabezpečíte, že sieťové zlyhania alebo chyby servera počas načítavania dát budú zachytené a elegantne spracované, čím zabránite pádu celej aplikácie. To poskytuje robustný a používateľsky prívetivý zážitok, umožňujúci používateľom pochopiť problém a potenciálne to skúsiť znova.

Správa stavu a invalidácia dát so Suspense

Je dôležité objasniť, že React Suspense primárne rieši počiatočný stav načítavania asynchrónnych zdrojov. Vnútorne nespravuje keš na strane klienta, nerieši invalidáciu dát, ani neorganizuje mutácie (operácie create, update, delete) a ich následné aktualizácie UI.

Tu sa stávajú nepostrádateľnými knižnice na načítavanie dát podporujúce Suspense (React Query, SWR, Apollo Client, Relay). Dopĺňajú Suspense tým, že poskytujú:

Bez robustnej knižnice na načítavanie dát by implementácia týchto funkcií nad manuálnym správcom zdrojov pre Suspense bola významným podnikom, v podstate by ste si museli vytvoriť vlastný framework na načítavanie dát.

Praktické úvahy a osvedčené postupy

Prijatie Suspense pre načítavanie dát je významné architektonické rozhodnutie. Tu sú niektoré praktické úvahy pre globálnu aplikáciu:

1. Nie všetky dáta potrebujú Suspense

Suspense je ideálny pre kritické dáta, ktoré priamo ovplyvňujú počiatočné renderovanie komponentu. Pre nekritické dáta, načítavanie na pozadí alebo dáta, ktoré je možné načítať lenivo bez silného vizuálneho dopadu, môže byť stále vhodný tradičný useEffect alebo pred-renderovanie. Nadmerné používanie Suspense môže viesť k menej granulárnemu zážitku z načítavania, keďže jedna hranica Suspense čaká na vyriešenie *všetkých* svojich detí.

2. Granularita hraníc Suspense

Umiestňujte svoje hranice <Suspense> premyslene. Jedna veľká hranica na vrchu vašej aplikácie môže skryť celú stránku za spinnerom, čo môže byť frustrujúce. Menšie, granulárnejšie hranice umožňujú rôznym častiam vašej stránky načítať sa nezávisle, poskytujúc progresívnejší a responzívnejší zážitok. Napríklad, hranica okolo komponentu profilu používateľa a ďalšia okolo zoznamu odporúčaných produktov.

<div>
  <h1>Stránka produktu</h1>
  <Suspense fallback={<p>Načítavajú sa hlavné detaily produktu...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>Súvisiace produkty</h2>
  <Suspense fallback={<p>Načítavajú sa súvisiace produkty...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

Tento prístup znamená, že používatelia môžu vidieť hlavné detaily produktu, aj keď sa súvisiace produkty stále načítavajú.

3. Server-Side Rendering (SSR) a streamovanie HTML

Nové streamovacie SSR API v React 18 (renderToPipeableStream) sa plne integrujú so Suspense. To umožňuje vášmu serveru posielať HTML hneď, ako je pripravené, aj keď sa časti stránky (ako komponenty závislé od dát) stále načítavajú. Server môže streamovať zástupný symbol (z fallbacku Suspense) a potom streamovať skutočný obsah, keď sa dáta vyriešia, bez potreby úplného pre-renderovania na strane klienta. To výrazne zlepšuje vnímaný výkon načítavania pre globálnych používateľov s rôznymi podmienkami siete.

4. Postupné prijatie

Nemusíte prepisovať celú svoju aplikáciu, aby ste mohli používať Suspense. Môžete ho zavádzať postupne, začínajúc novými funkciami alebo komponentmi, ktoré by najviac profitovali z jeho deklaratívnych vzorov načítavania.

5. Nástroje a ladenie

Zatiaľ čo Suspense zjednodušuje logiku komponentov, ladenie môže byť iné. React DevTools poskytujú prehľad o hraniciach Suspense a ich stavoch. Oboznámte sa s tým, ako vaša zvolená knižnica na načítavanie dát odhaľuje svoj interný stav (napr. React Query Devtools).

6. Časové limity pre záložné UI Suspense

Pri veľmi dlhých časoch načítavania by ste mohli chcieť zaviesť časový limit pre váš fallback Suspense, alebo po určitej dobe prejsť na podrobnejší indikátor načítavania. Hooky useDeferredValue a useTransition v React 18 môžu pomôcť spravovať tieto jemnejšie stavy načítavania, umožňujúc vám zobraziť 'starú' verziu UI, zatiaľ čo sa načítavajú nové dáta, alebo odložiť neurgentné aktualizácie.

Budúcnosť načítavania dát v Reacte: React Server Components a ďalej

Cesta načítavania dát v Reacte nekončí pri Suspense na strane klienta. React Server Components (RSC) predstavujú významnú evolúciu, sľubujúc stieranie hraníc medzi klientom a serverom a ďalšiu optimalizáciu načítavania dát.

Ako React pokračuje v dospievaní, Suspense bude čoraz centrálnejším dielom skladačky pre budovanie vysoko výkonných, používateľsky prívetivých a udržiavateľných aplikácií. Posúva vývojárov smerom k deklaratívnejšiemu a odolnejšiemu spôsobu narábania s asynchrónnymi operáciami, presúvajúc zložitosť z jednotlivých komponentov do dobre spravovanej dátovej vrstvy.

Záver

React Suspense, pôvodne funkcia pre delenie kódu, sa rozvinul do transformačného nástroja pre načítavanie dát. Prijatím vzoru Fetch-As-You-Render a využitím knižníc podporujúcich Suspense môžu vývojári výrazne zlepšiť používateľský zážitok svojich aplikácií, eliminovať vodopády načítavania, zjednodušiť logiku komponentov a poskytovať plynulé, koordinované stavy načítavania. V kombinácii s Error Boundaries pre robustné spracovanie chýb a budúcim prísľubom React Server Components nám Suspense umožňuje budovať aplikácie, ktoré sú nielen výkonné a odolné, ale aj vnútorne príjemnejšie pre používateľov po celom svete. Prechod na paradigmu načítavania dát riadenú Suspense si vyžaduje koncepčnú úpravu, ale výhody v oblasti zrozumiteľnosti kódu, výkonu a spokojnosti používateľov sú podstatné a stoja za investíciu.