Hrvatski

Istražite React Suspense za dohvaćanje podataka izvan 'code splittinga'. Razumijte Fetch-As-You-Render, rukovanje greškama i obrasce za budućnost globalnih aplikacija.

Učitavanje resursa pomoću React Suspensea: Ovladavanje modernim obrascima dohvaćanja podataka

U dinamičnom svijetu web razvoja, korisničko iskustvo (UX) je najvažnije. Očekuje se da su aplikacije brze, responzivne i ugodne za korištenje, bez obzira na mrežne uvjete ili mogućnosti uređaja. Za React developere, to se često prevodi u zamršeno upravljanje stanjem, složene indikatore učitavanja i stalnu borbu protiv vodopada pri dohvaćanju podataka. Tu nastupa React Suspense, moćna, iako često neshvaćena, značajka dizajnirana da temeljito transformira način na koji rukujemo asinkronim operacijama, osobito dohvaćanjem podataka.

Prvotno predstavljen za 'code splitting' s React.lazy(), pravi potencijal Suspensea leži u njegovoj sposobnosti orkestriranja učitavanja *bilo kojeg* asinkronog resursa, uključujući podatke s API-ja. Ovaj sveobuhvatni vodič detaljno će istražiti React Suspense za učitavanje resursa, istražujući njegove temeljne koncepte, fundamentalne obrasce dohvaćanja podataka i praktična razmatranja za izgradnju performantnih i otpornih globalnih aplikacija.

Evolucija dohvaćanja podataka u Reactu: Od imperativnog do deklarativnog

Dugi niz godina, dohvaćanje podataka u React komponentama uglavnom se oslanjalo na uobičajeni obrazac: korištenje useEffect hooka za pokretanje API poziva, upravljanje stanjima učitavanja i grešaka s useState, te uvjetno renderiranje na temelju tih stanja. Iako funkcionalan, ovaj pristup često je dovodio do nekoliko izazova:

Razmotrimo tipičan scenarij dohvaćanja podataka bez Suspensea:

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 greška! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Učitavanje korisničkog profila...</p>;
  }

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

  if (!user) {
    return <p>Nema dostupnih korisničkih podataka.</p>;
  }

  return (
    <div>
      <h2>Korisnik: {user.name}</h2>
      <p>Email: {user.email}</p>
      <!-- Više detalja o korisniku -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Dobrodošli u aplikaciju</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

Ovaj obrazac je sveprisutan, ali prisiljava komponentu da upravlja vlastitim asinkronim stanjem, što često dovodi do čvrsto povezane veze između korisničkog sučelja i logike dohvaćanja podataka. Suspense nudi deklarativniju i jednostavniju alternativu.

Razumijevanje React Suspensea izvan 'Code Splittinga'

Većina developera prvi put se susreće sa Suspenseom kroz React.lazy() za 'code splitting', gdje vam omogućuje da odgodite učitavanje koda komponente dok nije potreban. Na primjer:

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

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

function App() {
  return (
    <Suspense fallback={<div>Učitavanje komponente...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

U ovom scenariju, ako MyHeavyComponent još nije učitan, <Suspense> granica će uhvatiti 'promise' bačen od strane lazy() i prikazati fallback dok kod komponente ne bude spreman. Ključni uvid ovdje je da Suspense funkcionira tako što hvata 'promise' bačene tijekom renderiranja.

Ovaj mehanizam nije isključiv za učitavanje koda. Bilo koja funkcija pozvana tijekom renderiranja koja baci 'promise' (npr. zato što resurs još nije dostupan) može biti uhvaćena od strane Suspense granice više u stablu komponenata. Kada se 'promise' razriješi, React pokušava ponovno renderirati komponentu, i ako je resurs sada dostupan, 'fallback' se skriva i prikazuje se stvarni sadržaj.

Temeljni koncepti Suspensea za dohvaćanje podataka

Da bismo iskoristili Suspense za dohvaćanje podataka, moramo razumjeti nekoliko temeljnih principa:

1. Bacanje 'promisea'

Za razliku od tradicionalnog asinkronog koda koji koristi async/await za razrješavanje 'promisea', Suspense se oslanja na funkciju koja *baca* 'promise' ako podaci nisu spremni. Kada React pokuša renderirati komponentu koja poziva takvu funkciju, a podaci još čekaju, 'promise' se baca. React tada 'pauzira' renderiranje te komponente i njezinih potomaka, tražeći najbližu <Suspense> granicu.

2. Granica Suspensea

Komponenta <Suspense> djeluje kao granica grešaka za 'promisee'. Prima fallback prop, što je korisničko sučelje koje se renderira dok se bilo koji od njezinih potomaka (ili njihovih potomaka) suspendira (tj. baca 'promise'). Jednom kada se svi 'promisei' bačeni unutar njenog podstabla razriješe, 'fallback' se zamjenjuje stvarnim sadržajem.

Jedna Suspense granica može upravljati s više asinkronih operacija. Na primjer, ako imate dvije komponente unutar iste <Suspense> granice, i svaka treba dohvatiti podatke, 'fallback' će se prikazivati dok se *oba* dohvaćanja podataka ne završe. To izbjegava prikazivanje djelomičnog korisničkog sučelja i pruža koordiniranije iskustvo učitavanja.

3. Upravitelj predmemorije/resursa (odgovornost 'userlanda')

Ključno, sam Suspense ne upravlja dohvaćanjem podataka ni keširanjem. To je samo mehanizam za koordinaciju. Da bi Suspense radio za dohvaćanje podataka, potreban vam je sloj koji:

Ovaj 'upravitelj resursa' obično se implementira pomoću jednostavne predmemorije (npr. Map ili objekt) za pohranu stanja svakog resursa ('pending', 'resolved' ili 'errored'). Iako to možete izgraditi ručno u svrhu demonstracije, u stvarnoj aplikaciji koristili biste robusnu biblioteku za dohvaćanje podataka koja se integrira sa Suspenseom.

4. Konkurentni način rada (poboljšanja u Reactu 18)

Iako se Suspense može koristiti u starijim verzijama Reacta, njegova puna snaga oslobađa se s konkurentnim Reactom (omogućenim po defaultu u Reactu 18 s createRoot). Konkurentni način rada omogućuje Reactu da prekine, pauzira i nastavi rad na renderiranju. To znači:

Obrasci dohvaćanja podataka sa Suspenseom

Istražimo evoluciju obrazaca dohvaćanja podataka s dolaskom Suspensea.

Obrazac 1: Fetch-Then-Render (Tradicionalni s omotačem Suspensea)

Ovo je klasičan pristup gdje se podaci dohvaćaju, i tek onda se komponenta renderira. Iako ne koristi izravno mehanizam 'bacanja promisea' za podatke, možete omotati komponentu koja *na kraju* renderira podatke u Suspense granicu kako biste pružili 'fallback'. Ovdje se više radi o korištenju Suspensea kao generičkog orkestratora korisničkog sučelja za učitavanje za komponente koje na kraju postanu spremne, čak i ako je njihovo interno dohvaćanje podataka još uvijek temeljeno na tradicionalnom 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>Učitavanje detalja korisnika...</p>;
  }

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

function App() {
  return (
    <div>
      <h1>Fetch-Then-Render primjer</h1>
      <Suspense fallback={<div>Učitavanje cijele stranice...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

Prednosti: Jednostavan za razumijevanje, unatrag kompatibilan. Može se koristiti kao brz način za dodavanje globalnog stanja učitavanja.

Nedostaci: Ne eliminira ponavljajući kod unutar UserDetails. Još uvijek je sklon vodopadima ako komponente dohvaćaju podatke sekvencijalno. Ne iskorištava istinski Suspenseov 'throw-and-catch' mehanizam za same podatke.

Obrazac 2: Render-Then-Fetch (Dohvaćanje unutar renderiranja, nije za produkciju)

Ovaj obrazac prvenstveno služi za ilustraciju onoga što ne treba raditi izravno sa Suspenseom, jer može dovesti do beskonačnih petlji ili problema s performansama ako se ne rukuje pedantno. Uključuje pokušaj dohvaćanja podataka ili pozivanje suspendirajuće funkcije izravno unutar faze renderiranja komponente, *bez* odgovarajućeg mehanizma za keširanje.

// NE KORISTITI OVO U PRODUKCIJI BEZ ODGOVARAJUĆEG SLOJA ZA KEŠIRANJE
// Ovo je isključivo radi ilustracije kako bi izravno 'bacanje' moglo konceptualno funkcionirati.

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; // Ovdje Suspense stupa na scenu
}

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

function App() {
  return (
    <div>
      <h1>Render-Then-Fetch (Ilustrativno, NIJE preporučljivo izravno)</h1>
      <Suspense fallback={<div>Učitavanje korisnika...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

Prednosti: Prikazuje kako komponenta može izravno 'tražiti' podatke i suspendirati se ako nisu spremni.

Nedostaci: Vrlo problematično za produkciju. Ovaj ručni, globalni sustav s fetchedData i dataPromise je pojednostavljen, ne rukuje robusno s višestrukim zahtjevima, invalidacijom ili stanjima grešaka. To je primitivna ilustracija koncepta 'bacanja promisea', a ne obrazac koji treba usvojiti.

Obrazac 3: Fetch-As-You-Render (Idealni obrazac za Suspense)

Ovo je promjena paradigme koju Suspense uistinu omogućuje za dohvaćanje podataka. Umjesto čekanja da se komponenta renderira prije dohvaćanja podataka, ili dohvaćanja svih podataka unaprijed, Fetch-As-You-Render znači da počinjete dohvaćati podatke *što je prije moguće*, često *prije* ili *istovremeno s* procesom renderiranja. Komponente zatim 'čitaju' podatke iz predmemorije, i ako podaci nisu spremni, one se suspendiraju. Glavna ideja je odvojiti logiku dohvaćanja podataka od logike renderiranja komponente.

Za implementaciju Fetch-As-You-Render, potreban vam je mehanizam za:

  1. Pokretanje dohvaćanja podataka izvan funkcije renderiranja komponente (npr. kada se uđe na rutu ili se klikne gumb).
  2. Pohranjivanje 'promisea' ili razriješenih podataka u predmemoriju.
  3. Pružanje načina da komponente 'čitaju' iz te predmemorije. Ako podaci još nisu dostupni, funkcija čitanja baca 'pending' promise.

Ovaj obrazac rješava problem vodopada. Ako dvije različite komponente trebaju podatke, njihovi se zahtjevi mogu pokrenuti paralelno, a korisničko sučelje će se pojaviti tek kada su *oba* spremna, orkestrirano jednom Suspense granicom.

Ručna implementacija (radi razumijevanja)

Da biste shvatili temeljne mehanizme, stvorimo pojednostavljeni ručni upravitelj resursa. U stvarnoj aplikaciji, koristili biste namjensku biblioteku.

import React, { Suspense } from 'react';

// --- Jednostavan upravitelj predmemorije/resursa --- //
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);
}

// --- Funkcije za dohvaćanje podataka --- //
const fetchUserById = (id) => {
  console.log(`Dohvaćam korisnika ${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(`Dohvaćam objave za korisnika ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'Moja prva objava' }, { id: 'p2', title: 'Pustolovine s putovanja' }],
      '2': [{ id: 'p3', title: 'Uvidi u kodiranje' }],
      '3': [{ id: 'p4', title: 'Globalni trendovi' }, { id: 'p5', title: 'Lokalna kuhinja' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Komponente --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // Ovo će se suspendirati ako podaci o korisniku nisu spremni

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

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // Ovo će se suspendirati ako podaci o objavama nisu spremni

  return (
    <div>
      <h4>Objave korisnika {userId}:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>Nema pronađenih objava.</li>}
      </ul>
    </div>
  );
}

// --- Aplikacija --- //
let initialUserResource = null;
let initialPostsResource = null;

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

// Dohvati neke podatke unaprijed prije nego što se App komponenta uopće renderira
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Fetch-As-You-Render sa Suspenseom</h1>
      <p>Ovo demonstrira kako se dohvaćanje podataka može odvijati paralelno, koordinirano od strane Suspensea.</p>

      <Suspense fallback={<div>Učitavanje korisničkog profila i objava...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Drugi odjeljak</h2>
      <Suspense fallback={<div>Učitavanje drugog korisnika...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

U ovom primjeru:

Biblioteke za Fetch-As-You-Render

Ručna izrada i održavanje robusnog upravitelja resursa je složeno. Srećom, nekoliko zrelih biblioteka za dohvaćanje podataka je usvojilo ili usvaja Suspense, pružajući provjerena rješenja:

Ove biblioteke apstrahiraju složenost stvaranja i upravljanja resursima, rukovanja keširanjem, revalidacijom, optimističnim ažuriranjima i rukovanjem greškama, što znatno olakšava implementaciju Fetch-As-You-Render.

Obrazac 4: Pred-dohvaćanje (Prefetching) s bibliotekama svjesnim Suspensea

Pred-dohvaćanje je moćna optimizacija gdje proaktivno dohvaćate podatke koje će korisnik vjerojatno trebati u bliskoj budućnosti, prije nego što ih eksplicitno zatraži. To može drastično poboljšati percipirane performanse.

S bibliotekama svjesnim Suspensea, pred-dohvaćanje postaje besprijekorno. Možete pokrenuti dohvaćanje podataka na interakcijama korisnika koje ne mijenjaju odmah korisničko sučelje, kao što je prelazak mišem preko linka ili gumba.

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

// Pretpostavimo da su ovo vaši API pozivi
const fetchProductById = async (id) => {
  console.log(`Dohvaćam proizvod ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Globalni dodatak X', price: 29.99, description: 'Svestrani dodatak za međunarodnu upotrebu.' },
      'B002': { id: 'B002', name: 'Univerzalni gadget Y', price: 149.99, description: 'Najsuvremeniji gadget, omiljen diljem svijeta.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Omogući Suspense za sve upite po defaultu
    },
  },
});

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>Cijena: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // Dohvati podatke unaprijed kada korisnik prijeđe mišem preko linka proizvoda
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Pred-dohvaćanje proizvoda ${productId}`);
  };

  return (
    <div>
      <h2>Dostupni proizvodi:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Navigiraj ili prikaži detalje */ }}
          >Globalni dodatak X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Navigiraj ili prikaži detalje */ }}
          >Univerzalni gadget Y (B002)</a>
        </li>
      </ul>
      <p>Prijeđite mišem preko linka proizvoda da vidite pred-dohvaćanje na djelu. Otvorite mrežnu karticu da promatrate.</p>
    </div>
  );
}

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

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Pred-dohvaćanje s React Suspenseom (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Prikaži Globalni dodatak X</button>
      <button onClick={() => setShowProductB(true)}>Prikaži Univerzalni gadget Y</button>

      {showProductA && (
        <Suspense fallback={<p>Učitavanje Globalnog dodatka X...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Učitavanje Univerzalnog gadgeta Y...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

U ovom primjeru, prelazak mišem preko linka proizvoda pokreće `queryClient.prefetchQuery`, što inicira dohvaćanje podataka u pozadini. Ako korisnik zatim klikne gumb za prikaz detalja proizvoda, a podaci su već u predmemoriji od pred-dohvaćanja, komponenta će se odmah renderirati bez suspendiranja. Ako je pred-dohvaćanje još u tijeku ili nije pokrenuto, Suspense će prikazati rezervni sadržaj (fallback) dok podaci ne budu spremni.

Rukovanje greškama sa Suspenseom i granicama grešaka (Error Boundaries)

Dok Suspense upravlja stanjem 'učitavanja' prikazivanjem 'fallbacka', on ne upravlja izravno stanjima 'greške'. Ako 'promise' bačen od strane suspendirajuće komponente bude odbačen (tj. dohvaćanje podataka ne uspije), ova greška će se propagirati prema gore kroz stablo komponenata. Da biste graciozno rukovali ovim greškama i prikazali odgovarajuće korisničko sučelje, morate koristiti granice grešaka (Error Boundaries).

Granica grešaka je React komponenta koja implementira ili componentDidCatch ili static getDerivedStateFromError metode životnog ciklusa. Ona hvata JavaScript greške bilo gdje u svom podređenom stablu komponenata, uključujući greške bačene od strane 'promisea' koje bi Suspense inače uhvatio da su bile u stanju 'pending'.

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

// --- Komponenta za granicu grešaka --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Ažuriraj stanje tako da sljedeće renderiranje prikaže rezervno korisničko sučelje.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Također možete zabilježiti grešku u servisu za izvještavanje o greškama
    console.error("Uhvaćena greška:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Možete renderirati bilo koje prilagođeno rezervno korisničko sučelje
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Nešto je pošlo po zlu!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Molimo pokušajte osvježiti stranicu ili kontaktirajte podršku.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Pokušaj ponovno</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Dohvaćanje podataka (s mogućnošću greške) --- //
const fetchItemById = async (id) => {
  console.log(`Pokušavam dohvatiti stavku ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Nije uspjelo učitavanje stavke: Mreža nedostupna ili stavka nije pronađena.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Isporučeno sporo', data: 'Ova stavka je potrajala, ali je stigla!', status: 'success' });
    } else {
      resolve({ id, name: `Stavka ${id}`, data: `Podaci za stavku ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // Za demonstraciju, onemogućite ponovni pokušaj kako bi greška bila trenutna
    },
  },
});

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

  return (
    <div>
      <h3>Detalji stavke:</h3>
      <p>ID: {item.id}</p>
      <p>Naziv: {item.name}</p>
      <p>Podaci: {item.data}</p>
    </div>
  );
}

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

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense i granice grešaka</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Dohvati normalnu stavku</button>
        <button onClick={() => setFetchType('slow-item')}>Dohvati sporu stavku</button>
        <button onClick={() => setFetchType('error-item')}>Dohvati stavku s greškom</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Učitavanje stavke putem Suspensea...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

Omotavanjem vaše Suspense granice (ili komponenata koje bi mogle suspendirati) s granicom grešaka, osiguravate da su mrežni kvarovi ili greške poslužitelja tijekom dohvaćanja podataka uhvaćeni i graciozno obrađeni, sprječavajući rušenje cijele aplikacije. To pruža robusno i korisnički prijateljsko iskustvo, omogućujući korisnicima da razumiju problem i potencijalno ponove radnju.

Upravljanje stanjem i invalidacija podataka sa Suspenseom

Važno je pojasniti da React Suspense prvenstveno rješava početno stanje učitavanja asinkronih resursa. On inherentno ne upravlja predmemorijom na strani klijenta, ne rukuje invalidacijom podataka, niti orkestrira mutacije (operacije stvaranja, ažuriranja, brisanja) i njihova naknadna ažuriranja korisničkog sučelja.

Tu biblioteke za dohvaćanje podataka svjesne Suspensea (React Query, SWR, Apollo Client, Relay) postaju neophodne. One nadopunjuju Suspense pružajući:

Bez robusne biblioteke za dohvaćanje podataka, implementacija ovih značajki povrh ručnog upravitelja resursa za Suspense bio bi značajan pothvat, što bi u biti zahtijevalo da izgradite vlastiti okvir za dohvaćanje podataka.

Praktična razmatranja i najbolje prakse

Usvajanje Suspensea za dohvaćanje podataka je značajna arhitektonska odluka. Evo nekoliko praktičnih razmatranja za globalnu aplikaciju:

1. Ne trebaju svi podaci Suspense

Suspense je idealan za kritične podatke koji izravno utječu na početno renderiranje komponente. Za ne-kritične podatke, dohvaćanja u pozadini ili podatke koji se mogu lijeno učitati bez jakog vizualnog utjecaja, tradicionalni useEffect ili pred-renderiranje i dalje mogu biti prikladni. Prekomjerna upotreba Suspensea može dovesti do manje granularnog iskustva učitavanja, jer jedna Suspense granica čeka da se *svi* njezini potomci razriješe.

2. Granularnost Suspense granica

Promišljeno postavite svoje <Suspense> granice. Jedna, velika granica na vrhu vaše aplikacije mogla bi sakriti cijelu stranicu iza 'spinnera', što može biti frustrirajuće. Manje, granularnije granice omogućuju da se različiti dijelovi vaše stranice učitavaju neovisno, pružajući progresivnije i responzivnije iskustvo. Na primjer, granica oko komponente korisničkog profila, i druga oko popisa preporučenih proizvoda.

<div>
  <h1>Stranica proizvoda</h1>
  <Suspense fallback={<p>Učitavanje glavnih detalja proizvoda...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>Povezani proizvodi</h2>
  <Suspense fallback={<p>Učitavanje povezanih proizvoda...</p>}>
    <RelatedProducts category="elektronika" />
  </Suspense>
</div>

Ovaj pristup znači da korisnici mogu vidjeti glavne detalje proizvoda čak i ako se povezani proizvodi još uvijek učitavaju.

3. Renderiranje na strani poslužitelja (SSR) i 'Streaming' HTML-a

Novi API-ji za 'streaming' SSR u Reactu 18 (renderToPipeableStream) potpuno se integriraju sa Suspenseom. To omogućuje vašem poslužitelju da pošalje HTML čim je spreman, čak i ako se dijelovi stranice (poput komponenata ovisnih o podacima) još uvijek učitavaju. Poslužitelj može streamati 'placeholder' (iz Suspense 'fallbacka'), a zatim streamati stvarni sadržaj kada se podaci razriješe, bez potrebe za potpunim ponovnim renderiranjem na strani klijenta. To značajno poboljšava percipirane performanse učitavanja za globalne korisnike pod različitim mrežnim uvjetima.

4. Postupno usvajanje

Ne morate prepisivati cijelu aplikaciju da biste koristili Suspense. Možete ga uvoditi postupno, počevši s novim značajkama ili komponentama koje bi najviše profitirale od njegovih deklarativnih obrazaca učitavanja.

5. Alati i otklanjanje grešaka (Debugging)

Iako Suspense pojednostavljuje logiku komponente, otklanjanje grešaka može biti drugačije. React DevTools pružaju uvide u Suspense granice i njihova stanja. Upoznajte se s načinom na koji vaša odabrana biblioteka za dohvaćanje podataka izlaže svoje interno stanje (npr. React Query Devtools).

6. Vremenska ograničenja za Suspense 'fallbackove'

Za vrlo duga vremena učitavanja, možda ćete htjeti uvesti vremensko ograničenje za vaš Suspense 'fallback', ili se prebaciti na detaljniji indikator učitavanja nakon određenog kašnjenja. Hookovi useDeferredValue i useTransition u Reactu 18 mogu pomoći u upravljanju ovim nijansiranijim stanjima učitavanja, omogućujući vam da prikažete 'staru' verziju korisničkog sučelja dok se novi podaci dohvaćaju, ili odgoditi ne-hitna ažuriranja.

Budućnost dohvaćanja podataka u Reactu: React Server Components i dalje

Putovanje dohvaćanja podataka u Reactu ne završava s klijentskim Suspenseom. React Server Components (RSC) predstavljaju značajnu evoluciju, obećavajući da će zamagliti granice između klijenta i poslužitelja, i dodatno optimizirati dohvaćanje podataka.

Kako React nastavlja sazrijevati, Suspense će biti sve središnji dio slagalice za izgradnju visoko performantnih, korisnički prijateljskih i održivih aplikacija. On potiče developere prema deklarativnijem i otpornijem načinu rukovanja asinkronim operacijama, prebacujući složenost s pojedinačnih komponenata u dobro upravljani podatkovni sloj.

Zaključak

React Suspense, prvotno značajka za 'code splitting', procvjetao je u transformativni alat za dohvaćanje podataka. Prihvaćanjem obrasca Fetch-As-You-Render i korištenjem biblioteka svjesnih Suspensea, developeri mogu značajno poboljšati korisničko iskustvo svojih aplikacija, eliminirajući vodopade učitavanja, pojednostavljujući logiku komponenata i pružajući glatka, koordinirana stanja učitavanja. U kombinaciji s granicama grešaka za robusno rukovanje greškama i budućim obećanjem React Server Components, Suspense nas osnažuje da gradimo aplikacije koje nisu samo performantne i otporne, već i inherentno ugodnije za korisnike diljem svijeta. Prijelaz na paradigmu dohvaćanja podataka vođenu Suspenseom zahtijeva konceptualnu prilagodbu, ali prednosti u smislu jasnoće koda, performansi i zadovoljstva korisnika su značajne i itekako vrijedne ulaganja.