Lietuvių

Atraskite „React Suspense“ duomenų gavimui ne tik kodo padalijimui. Supraskite „Fetch-As-You-Render“, klaidų valdymą ir ateities globalių programų modelius.

React Suspense išteklių įkėlimas: šiuolaikinių duomenų gavimo modelių įsisavinimas

Dinamiškame žiniatinklio kūrimo pasaulyje svarbiausia yra vartotojo patirtis (UX). Tikimasi, kad programos bus greitos, jautrios ir malonios naudoti, nepriklausomai nuo tinklo sąlygų ar įrenginio galimybių. „React“ kūrėjams tai dažnai reiškia sudėtingą būsenos valdymą, sudėtingus įkėlimo indikatorius ir nuolatinę kovą su duomenų gavimo kriokliais. Pristatome „React Suspense“ – galingą, nors dažnai neteisingai suprantamą funkciją, skirtą iš esmės pakeisti, kaip tvarkome asinchronines operacijas, ypač duomenų gavimą.

Iš pradžių pristatyta kodo padalijimui su React.lazy(), tikrasis „Suspense“ potencialas slypi jo gebėjime organizuoti *bet kokio* asinchroninio ištekliaus, įskaitant duomenis iš API, įkėlimą. Šis išsamus vadovas gilinsis į „React Suspense“ išteklių įkėlimui, tyrinės jo pagrindines koncepcijas, esminius duomenų gavimo modelius ir praktinius aspektus kuriant našias ir atsparias globalias programas.

Duomenų gavimo evoliucija „React“: nuo imperatyvaus iki deklaratyvaus

Daugelį metų duomenų gavimas „React“ komponentuose daugiausia rėmėsi įprastu modeliu: naudojant useEffect „hook'ą“ inicijuoti API iškvietimą, valdant įkėlimo ir klaidų būsenas su useState ir sąlygiškai atvaizduojant remiantis šiomis būsenomis. Nors ir funkcionalus, šis požiūris dažnai sukeldavo keletą iššūkių:

Panagrinėkime tipišką duomenų gavimo scenarijų be „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>Loading user profile...</p>;
  }

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

  if (!user) {
    return <p>No user data available.</p>;
  }

  return (
    <div>
      <h2>User: {user.name}</h2>
      <p>Email: {user.email}</p>
      <!-- More user details -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Welcome to the Application</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

Šis modelis yra visur paplitęs, tačiau jis priverčia komponentą valdyti savo asinchroninę būseną, dažnai sukuriant glaudų ryšį tarp vartotojo sąsajos ir duomenų gavimo logikos. „Suspense“ siūlo deklaratyvesnę ir supaprastintą alternatyvą.

„React Suspense“ supratimas ne tik kodo padalijimui

Dauguma kūrėjų pirmą kartą susiduria su „Suspense“ per React.lazy() kodo padalijimui, kai leidžiama atidėti komponento kodo įkėlimą, kol jis bus reikalingas. Pavyzdžiui:

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

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

function App() {
  return (
    <Suspense fallback={<div>Loading component...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Šiame scenarijuje, jei MyHeavyComponent dar nėra įkeltas, <Suspense> riba pagaus pažadą (promise), kurį išmeta lazy(), ir rodys fallback, kol komponento kodas bus paruoštas. Svarbiausia įžvalga čia yra ta, kad „Suspense“ veikia gaudydamas pažadus, išmestus atvaizdavimo metu.

Šis mechanizmas nėra išskirtinis tik kodo įkėlimui. Bet kuri funkcija, iškviesta atvaizdavimo metu, kuri išmeta pažadą (pvz., nes išteklius dar neprieinamas), gali būti pagauta „Suspense“ ribos aukščiau komponentų medyje. Kai pažadas išsipildo, „React“ bando iš naujo atvaizduoti komponentą, ir jei išteklius dabar yra prieinamas, atsarginis turinys yra paslepiamas, o tikrasis turinys yra rodomas.

Pagrindinės „Suspense“ koncepcijos duomenų gavimui

Norėdami pasinaudoti „Suspense“ duomenų gavimui, turime suprasti keletą pagrindinių principų:

1. Pažado išmetimas (Throwing a Promise)

Skirtingai nuo tradicinio asinchroninio kodo, kuris naudoja async/await pažadams išspręsti, „Suspense“ remiasi funkcija, kuri *išmeta* pažadą, jei duomenys nėra paruošti. Kai „React“ bando atvaizduoti komponentą, kuris kviečia tokią funkciją, o duomenys vis dar laukiami, pažadas yra išmetamas. „React“ tada „pristabdo“ to komponento ir jo vaikų atvaizdavimą, ieškodamas artimiausios <Suspense> ribos.

2. „Suspense“ riba

Komponentas <Suspense> veikia kaip klaidų riba pažadams. Jis priima fallback savybę, kuri yra vartotojo sąsaja, kurią reikia atvaizduoti, kol bet kuris iš jo vaikų (ar jų palikuonių) yra sustabdytas (t. y., meta pažadą). Kai visi pažadai, išmesti jo medyje, išsipildo, atsarginis turinys pakeičiamas tikruoju turiniu.

Viena „Suspense“ riba gali valdyti kelias asinchronines operacijas. Pavyzdžiui, jei turite du komponentus toje pačioje <Suspense> riboje ir kiekvienam reikia gauti duomenis, atsarginis turinys bus rodomas, kol bus baigti *abu* duomenų gavimo procesai. Tai leidžia išvengti dalinės vartotojo sąsajos rodymo ir suteikia labiau koordinuotą įkėlimo patirtį.

3. Podėlio/Išteklių valdytojas (Userland Responsibility)

Svarbu tai, kad pats „Suspense“ netvarko duomenų gavimo ar podėliavimo. Tai tik koordinavimo mechanizmas. Kad „Suspense“ veiktų su duomenų gavimu, jums reikia sluoksnio, kuris:

Šis „išteklių valdytojas“ paprastai yra įgyvendinamas naudojant paprastą podėlį (pvz., „Map“ ar objektą), kad būtų saugoma kiekvieno ištekliaus būsena (laukia, išspręsta ar klaida). Nors demonstraciniais tikslais galite tai sukurti rankiniu būdu, realioje programoje naudotumėte tvirtą duomenų gavimo biblioteką, kuri integruojasi su „Suspense“.

4. Lygiagretusis režimas („React 18“ patobulinimai)

Nors „Suspense“ galima naudoti senesnėse „React“ versijose, visa jo galia atsiskleidžia su „Concurrent React“ (numatytasis „React 18“ su createRoot). Lygiagretusis režimas leidžia „React“ pertraukti, pristabdyti ir tęsti atvaizdavimo darbus. Tai reiškia:

Duomenų gavimo modeliai su „Suspense“

Panagrinėkime duomenų gavimo modelių evoliuciją atsiradus „Suspense“.

1 modelis: Gauti-tada-atvaizduoti (tradicinis su „Suspense“ apgaubimu)

Tai klasikinis požiūris, kai duomenys gaunami, ir tik tada komponentas atvaizduojamas. Nors tiesiogiai nenaudojamas „išmesti pažadą“ mechanizmas duomenims, galite apgaubti komponentą, kuris *galiausiai* atvaizduoja duomenis, „Suspense“ riboje, kad pateiktumėte atsarginį turinį. Tai labiau susiję su „Suspense“ naudojimu kaip bendro įkėlimo UI orkestratoriumi komponentams, kurie galiausiai tampa paruošti, net jei jų vidinis duomenų gavimas vis dar pagrįstas tradiciniu 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>Loading user details...</p>;
  }

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

function App() {
  return (
    <div>
      <h1>Fetch-Then-Render Example</h1>
      <Suspense fallback={<div>Overall page loading...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

Privalumai: Paprasta suprasti, atgalinis suderinamumas. Gali būti naudojamas kaip greitas būdas pridėti globalią įkėlimo būseną.

Trūkumai: Nepašalina šabloninio kodo UserDetails viduje. Vis dar linkęs į krioklius, jei komponentai gauna duomenis nuosekliai. Iš tikrųjų nenaudoja „Suspense“ „išmesti-ir-pagauti“ mechanizmo patiems duomenims.

2 modelis: Atvaizduoti-tada-gauti (gavimas atvaizdavimo metu, netinka produkcijai)

Šis modelis daugiausia skirtas iliustruoti, ko nedaryti su „Suspense“ tiesiogiai, nes tai gali sukelti begalines kilpas ar našumo problemas, jei nebus kruopščiai valdoma. Tai apima bandymą gauti duomenis arba kviesti sustabdančią funkciją tiesiogiai komponento atvaizdavimo fazėje, *be* tinkamo podėliavimo mechanizmo.

// NENAUDOKITE TO PRODUKCIJOJE BE TINKAMO PODĖLIAVIMO SLUOKSNIO
// Tai tik iliustracija, kaip konceptualiai galėtų veikti tiesioginis 'throw'.

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; // Čia įsijungia Suspense
}

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

function App() {
  return (
    <div>
      <h1>Render-Then-Fetch (Illustrative, NOT Recommended Directly)</h1>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

Privalumai: Parodo, kaip komponentas gali tiesiogiai „prašyti“ duomenų ir sustabdyti vykdymą, jei jie neparuošti.

Trūkumai: Labai problematiška produkcijai. Ši rankinė, globali fetchedData ir dataPromise sistema yra primityvi, netvarko kelių užklausų, invalidavimo ar klaidų būsenų tvirtai. Tai yra primityvi „išmesti-pažadą“ koncepcijos iliustracija, o ne modelis, kurį reikėtų taikyti.

3 modelis: Gauti-atvaizduojant (Fetch-As-You-Render – idealus „Suspense“ modelis)

Tai yra paradigmos pokytis, kurį „Suspense“ iš tiesų įgalina duomenų gavimui. Užuot laukus, kol komponentas bus atvaizduotas, prieš pradedant gauti jo duomenis, arba gaunant visus duomenis iš anksto, „Gauti-atvaizduojant“ reiškia, kad pradedate gauti duomenis *kuo greičiau*, dažnai *prieš* arba *lygiagrečiai su* atvaizdavimo procesu. Tada komponentai „skaito“ duomenis iš podėlio, ir jei duomenys dar neparuošti, jie sustabdo vykdymą. Pagrindinė idėja yra atskirti duomenų gavimo logiką nuo komponento atvaizdavimo logikos.

Norint įgyvendinti „Gauti-atvaizduojant“, jums reikia mechanizmo, kuris:

  1. Inicijuoja duomenų gavimą ne komponento atvaizdavimo funkcijoje (pvz., kai įeinama į maršrutą arba paspaudžiamas mygtukas).
  2. Saugo pažadą arba išspręstus duomenis podėlyje.
  3. Suteikia būdą komponentams „skaityti“ iš šio podėlio. Jei duomenys dar neprieinami, skaitymo funkcija išmeta laukiantį pažadą.

Šis modelis sprendžia krioklio problemą. Jei dviem skirtingiems komponentams reikia duomenų, jų užklausos gali būti inicijuotos lygiagrečiai, o vartotojo sąsaja pasirodys tik tada, kai *abu* bus paruošti, koordinuojami vienos „Suspense“ ribos.

Rankinis įgyvendinimas (supratimui)

Norėdami suprasti pagrindinius mechanizmus, sukurkime supaprastintą rankinį išteklių valdytoją. Realioje programoje naudotumėte specializuotą biblioteką.

import React, { Suspense } from 'react';

// --- Paprastas podėlio/išteklių valdytojas --- //
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);
}

// --- Duomenų gavimo funkcijos --- //
const fetchUserById = (id) => {
  console.log(`Fetching user ${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(`Fetching posts for user ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'My First Post' }, { id: 'p2', title: 'Travel Adventures' }],
      '2': [{ id: 'p3', title: 'Coding Insights' }],
      '3': [{ id: 'p4', title: 'Global Trends' }, { id: 'p5', title: 'Local Cuisine' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Komponentai --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // Tai sustabdys vykdymą, jei vartotojo duomenys neparuošti

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

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // Tai sustabdys vykdymą, jei įrašų duomenys neparuošti

  return (
    <div>
      <h4>Posts by {userId}:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>No posts found.</li>}
      </ul>
    </div>
  );
}

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

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

// Iš anksto gauti kai kuriuos duomenis dar prieš atvaizduojant App komponentą
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Fetch-As-You-Render with Suspense</h1>
      <p>This demonstrates how data fetching can happen in parallel, coordinated by Suspense.</p>

      <Suspense fallback={<div>Loading user profile and posts...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Another Section</h2>
      <Suspense fallback={<div>Loading different user...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

Šiame pavyzdyje:

Bibliotekos, skirtos „Gauti-atvaizduojant“

Sukurti ir prižiūrėti tvirtą išteklių valdytoją rankiniu būdu yra sudėtinga. Laimei, kelios brandžios duomenų gavimo bibliotekos pritaikė arba pritaiko „Suspense“, siūlydamos mūšyje išbandytus sprendimus:

Šios bibliotekos abstrahuoja išteklių kūrimo ir valdymo, podėliavimo, pakartotinio patvirtinimo, optimistinių atnaujinimų ir klaidų valdymo sudėtingumą, todėl įgyvendinti „Gauti-atvaizduojant“ tampa daug lengviau.

4 modelis: Išankstinis gavimas su „Suspense“ palaikančiomis bibliotekomis

Išankstinis gavimas (prefetching) yra galinga optimizacija, kai aktyviai gaunate duomenis, kurių vartotojui greičiausiai prireiks artimiausiu metu, dar prieš jam aiškiai jų paprašant. Tai gali drastiškai pagerinti suvokiamą našumą.

Su „Suspense“ palaikančiomis bibliotekomis išankstinis gavimas tampa sklandus. Galite inicijuoti duomenų gavimą reaguodami į vartotojo veiksmus, kurie iš karto nekeičia vartotojo sąsajos, pavyzdžiui, užvedus pelės žymeklį ant nuorodos ar mygtuko.

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

// Assume these are your API calls
const fetchProductById = async (id) => {
  console.log(`Fetching product ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'A versatile widget for international use.' },
      'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Cutting-edge gadget, loved worldwide.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Enable Suspense for all queries by default
    },
  },
});

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

function ProductList() {
  const handleProductHover = (productId) => {
    // Prefetch data when a user hovers over a product link
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Prefetching product ${productId}`);
  };

  return (
    <div>
      <h2>Available Products:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
          >Global Widget X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
          >Universal Gadget Y (B002)</a>
        </li>
      </ul>
      <p>Hover over a product link to see prefetching in action. Open network tab to observe.</p>
    </div>
  );
}

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

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Prefetching with React Suspense (React Query)</h1>
      <ProductList />

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

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

Šiame pavyzdyje, užvedus pelės žymeklį ant produkto nuorodos, paleidžiama `queryClient.prefetchQuery`, kuri inicijuoja duomenų gavimą fone. Jei vartotojas tada paspaudžia mygtuką, kad parodytų produkto detales, ir duomenys jau yra podėlyje iš išankstinio gavimo, komponentas bus atvaizduotas iš karto be sustabdymo. Jei išankstinis gavimas dar vyksta arba nebuvo inicijuotas, „Suspense“ parodys atsarginį turinį, kol duomenys bus paruošti.

Klaidų valdymas su „Suspense“ ir klaidų ribomis (Error Boundaries)

Nors „Suspense“ valdo „įkėlimo“ būseną rodydamas atsarginį turinį, jis tiesiogiai nevaldo „klaidos“ būsenų. Jei pažadas, išmestas sustabdančio komponento, yra atmetamas (t. y., duomenų gavimas nepavyksta), ši klaida bus perduota aukštyn komponentų medžiu. Norėdami grakščiai tvarkyti šias klaidas ir parodyti tinkamą vartotojo sąsają, turite naudoti klaidų ribas (Error Boundaries).

Klaidų riba yra „React“ komponentas, kuris įgyvendina arba componentDidCatch, arba static getDerivedStateFromError gyvavimo ciklo metodus. Ji pagauna „JavaScript“ klaidas bet kurioje savo vaikų komponentų medžio vietoje, įskaitant klaidas, kurias išmeta pažadai, kuriuos „Suspense“ paprastai pagautų, jei jie būtų laukiami.

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

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

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Something went wrong!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Please try refreshing the page or contact support.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Try Again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Data Fetching (with potential for error) --- //
const fetchItemById = async (id) => {
  console.log(`Attempting to fetch item ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Failed to load item: Network unreachable or item not found.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Delivered Slowly', data: 'This item took a while but arrived!', status: 'success' });
    } else {
      resolve({ id, name: `Item ${id}`, data: `Data for item ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // For demonstration, disable retry so error is immediate
    },
  },
});

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

  return (
    <div>
      <h3>Item Details:</h3>
      <p>ID: {item.id}</p>
      <p>Name: {item.name}</p>
      <p>Data: {item.data}</p>
    </div>
  );
}

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

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

      <div>
        <button onClick={() => setFetchType('normal-item')}>Fetch Normal Item</button>
        <button onClick={() => setFetchType('slow-item')}>Fetch Slow Item</button>
        <button onClick={() => setFetchType('error-item')}>Fetch Error Item</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Loading item via Suspense...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

Apgaubdami savo „Suspense“ ribą (arba komponentus, kurie gali sustabdyti vykdymą) klaidų riba, užtikrinate, kad tinklo gedimai ar serverio klaidos duomenų gavimo metu būtų pagautos ir grakščiai sutvarkytos, užkertant kelią visos programos sutrikimui. Tai suteikia tvirtą ir vartotojui draugišką patirtį, leidžiančią vartotojams suprasti problemą ir galbūt bandyti iš naujo.

Būsenos valdymas ir duomenų invalidavimas su „Suspense“

Svarbu paaiškinti, kad „React Suspense“ pirmiausia sprendžia pradinę asinchroninių išteklių įkėlimo būseną. Jis savaime nevaldo kliento pusės podėlio, netvarko duomenų invalidavimo ar neorganizuoja mutacijų (kūrimo, atnaujinimo, trynimo operacijų) ir jų vėlesnių UI atnaujinimų.

Čia „Suspense“ palaikančios duomenų gavimo bibliotekos („React Query“, SWR, „Apollo Client“, „Relay“) tampa nepakeičiamos. Jos papildo „Suspense“ teikdamos:

Be tvirtos duomenų gavimo bibliotekos, šių funkcijų įgyvendinimas ant rankinio „Suspense“ išteklių valdytojo būtų didelis uždavinys, iš esmės reikalaujantis sukurti savo duomenų gavimo sistemą.

Praktiniai aspektai ir gerosios praktikos

Priimti „Suspense“ duomenų gavimui yra svarbus architektūrinis sprendimas. Štai keletas praktinių aspektų globaliai programai:

1. Ne visiems duomenims reikia „Suspense“

„Suspense“ idealiai tinka kritiniams duomenims, kurie tiesiogiai veikia pradinį komponento atvaizdavimą. Nekritiniams duomenims, foniniams gavimams ar duomenims, kuriuos galima įkelti tingiai be didelio vizualinio poveikio, tradicinis useEffect ar išankstinis atvaizdavimas vis dar gali būti tinkamas. Per didelis „Suspense“ naudojimas gali lemti mažiau detalizuotą įkėlimo patirtį, nes viena „Suspense“ riba laukia, kol *visi* jos vaikai bus išspręsti.

2. „Suspense“ ribų detalumas

Apgalvotai išdėstykite savo <Suspense> ribas. Viena didelė riba programos viršuje gali paslėpti visą puslapį už besisukančio indikatoriaus, o tai gali būti nemalonu. Mažesnės, detalesnės ribos leidžia skirtingoms puslapio dalims įsikelti nepriklausomai, suteikiant laipsnišką ir jautresnę patirtį. Pavyzdžiui, viena riba aplink vartotojo profilio komponentą, o kita – aplink rekomenduojamų produktų sąrašą.

<div>
  <h1>Product Page</h1>
  <Suspense fallback={<p>Loading main product details...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>Related Products</h2>
  <Suspense fallback={<p>Loading related products...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

Šis požiūris reiškia, kad vartotojai gali matyti pagrindinę produkto informaciją, net jei susiję produktai vis dar įkeliami.

3. Serverio pusės atvaizdavimas (SSR) ir srautinis HTML

Naujos „React 18“ srautinio SSR API (renderToPipeableStream) visiškai integruojasi su „Suspense“. Tai leidžia jūsų serveriui siųsti HTML, kai tik jis yra paruoštas, net jei kai kurios puslapio dalys (pvz., nuo duomenų priklausomi komponentai) vis dar įkeliamos. Serveris gali perduoti srautu vietos rezervavimo ženklą (iš „Suspense“ atsarginio turinio), o vėliau perduoti srautu tikrąjį turinį, kai duomenys išsisprendžia, nereikalaujant pilno kliento pusės pakartotinio atvaizdavimo. Tai žymiai pagerina suvokiamą įkėlimo našumą globaliems vartotojams esant įvairioms tinklo sąlygoms.

4. Laipsniškas pritaikymas

Jums nereikia perrašyti visos savo programos, kad galėtumėte naudoti „Suspense“. Galite jį įdiegti laipsniškai, pradedant nuo naujų funkcijų ar komponentų, kurie labiausiai pasinaudotų jo deklaratyviais įkėlimo modeliais.

5. Įrankiai ir derinimas

Nors „Suspense“ supaprastina komponentų logiką, derinimas gali būti kitoks. „React DevTools“ suteikia įžvalgų apie „Suspense“ ribas ir jų būsenas. Susipažinkite, kaip jūsų pasirinkta duomenų gavimo biblioteka atskleidžia savo vidinę būseną (pvz., „React Query Devtools“).

6. „Suspense“ atsarginio turinio laiko limitai

Labai ilgiems įkėlimo laikams galbūt norėsite įvesti laiko limitą savo „Suspense“ atsarginiam turiniui arba po tam tikro vėlavimo pereiti prie detalesnio įkėlimo indikatoriaus. „React 18“ „hook'ai“ useDeferredValue ir useTransition gali padėti valdyti šias subtilesnes įkėlimo būsenas, leidžiant parodyti „seną“ UI versiją, kol gaunami nauji duomenys, arba atidėti neskubius atnaujinimus.

Duomenų gavimo ateitis „React“: „React Server Components“ ir toliau

Duomenų gavimo kelionė „React“ nesibaigia su kliento pusės „Suspense“. „React Server Components“ (RSC) yra reikšminga evoliucija, žadanti ištrinti ribas tarp kliento ir serverio ir dar labiau optimizuoti duomenų gavimą.

„React“ toliau bręstant, „Suspense“ taps vis svarbesne galvosūkio dalimi kuriant labai našias, vartotojui draugiškas ir prižiūrimas programas. Tai skatina kūrėjus pereiti prie deklaratyvesnio ir atsparesnio asinchroninių operacijų tvarkymo būdo, perkeliant sudėtingumą iš atskirų komponentų į gerai valdomą duomenų sluoksnį.

Išvada

„React Suspense“, iš pradžių buvusi funkcija kodo padalijimui, išaugo į transformuojantį įrankį duomenų gavimui. Priimdami „Gauti-atvaizduojant“ modelį ir pasinaudodami „Suspense“ palaikančiomis bibliotekomis, kūrėjai gali žymiai pagerinti savo programų vartotojo patirtį, pašalindami įkėlimo krioklius, supaprastindami komponentų logiką ir teikdami sklandžias, koordinuotas įkėlimo būsenas. Kartu su klaidų ribomis tvirtam klaidų valdymui ir ateities pažadu – „React Server Components“, „Suspense“ suteikia mums galią kurti programas, kurios yra ne tik našios ir atsparios, bet ir iš prigimties malonesnės vartotojams visame pasaulyje. Perėjimas prie „Suspense“ pagrįstos duomenų gavimo paradigmos reikalauja konceptualaus prisitaikymo, tačiau nauda kodo aiškumo, našumo ir vartotojų pasitenkinimo požiūriu yra didelė ir verta investicijų.

React Suspense išteklių įkėlimas: šiuolaikinių duomenų gavimo modelių įsisavinimas | MLOG