Latviešu

Izpētiet React Suspense datu ielādei ārpus koda sadalīšanas. Izprotiet Fetch-As-You-Render, kļūdu apstrādi un nākotnes drošus modeļus globālām lietotnēm.

React Suspense resursu ielāde: Mūsdienu datu ielādes modeļu apgūšana

Dinamiskajā tīmekļa izstrādes pasaulē lietotāja pieredze (UX) ir vissvarīgākā. Tiek sagaidīts, ka lietotnes būs ātras, atsaucīgas un patīkamas, neatkarīgi no tīkla apstākļiem vai ierīces iespējām. React izstrādātājiem tas bieži vien nozīmē sarežģītu stāvokļa pārvaldību, kompleksus ielādes indikatorus un pastāvīgu cīņu pret datu ielādes ūdenskritumiem (waterfalls). Un te parādās React Suspense — spēcīga, lai gan bieži vien pārprasta, funkcija, kas izstrādāta, lai fundamentāli mainītu to, kā mēs apstrādājam asinhronas darbības, īpaši datu ielādi.

Sākotnēji ieviesta koda sadalīšanai ar React.lazy(), Suspense patiesais potenciāls slēpjas tā spējā organizēt *jebkura* asinhrona resursa ielādi, ieskaitot datus no API. Šī visaptverošā rokasgrāmata padziļināti aplūkos React Suspense resursu ielādei, izpētot tā pamatkoncepcijas, fundamentālos datu ielādes modeļus un praktiskus apsvērumus, lai veidotu veiktspējīgas un noturīgas globālas lietotnes.

Datu ielādes evolūcija React: No imperatīvās uz deklaratīvo pieeju

Daudzus gadus datu ielāde React komponentos galvenokārt balstījās uz ierastu modeli: izmantojot useEffect hook, lai uzsāktu API izsaukumu, pārvaldot ielādes un kļūdu stāvokļus ar useState un nosacīti renderējot, pamatojoties uz šiem stāvokļiem. Lai gan šī pieeja bija funkcionāla, tā bieži radīja vairākas problēmas:

Apsveriet tipisku datu ielādes scenāriju 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 kļūda! statuss: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Ielādē lietotāja profilu...</p>;
  }

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

  if (!user) {
    return <p>Nav pieejami lietotāja dati.</p>;
  }

  return (
    <div>
      <h2>Lietotājs: {user.name}</h2>
      <p>E-pasts: {user.email}</p>
      <!-- Vairāk lietotāja detaļu -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Laipni lūdzam lietotnē</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

Šis modelis ir visuresošs, bet tas liek komponentam pašam pārvaldīt savu asinhrono stāvokli, bieži vien novedot pie cieši saistītas attiecības starp UI un datu ielādes loģiku. Suspense piedāvā deklaratīvāku un racionalizētāku alternatīvu.

Izpratne par React Suspense ārpus koda sadalīšanas

Vairums izstrādātāju pirmo reizi saskaras ar Suspense, izmantojot React.lazy() koda sadalīšanai, kur tas ļauj atlikt komponenta koda ielādi līdz brīdim, kad tas ir nepieciešams. Piemēram:

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

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

function App() {
  return (
    <Suspense fallback={<div>Ielādē komponentu...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Šajā scenārijā, ja MyHeavyComponent vēl nav ielādēts, <Suspense> robeža notvers solījumu (promise), ko izmet lazy(), un parādīs fallback, līdz komponenta kods būs gatavs. Galvenā atziņa šeit ir tā, ka Suspense darbojas, notverot renderēšanas laikā izmestus solījumus.

Šis mehānisms nav ekskluzīvs tikai koda ielādei. Jebkura funkcija, kas tiek izsaukta renderēšanas laikā un kas izmet solījumu (piemēram, jo resurss vēl nav pieejams), var tikt notverta ar Suspense robežu augstāk komponentu kokā. Kad solījums atrisinās, React mēģina atkārtoti renderēt komponentu, un, ja resurss tagad ir pieejams, fallback tiek paslēpts un tiek parādīts faktiskais saturs.

Suspense pamatkoncepcijas datu ielādei

Lai izmantotu Suspense datu ielādei, mums jāizprot daži pamatprincipi:

1. Solījuma izmešana (Throwing a Promise)

Atšķirībā no tradicionālā asinhronā koda, kas izmanto async/await, lai atrisinātu solījumus, Suspense paļaujas uz funkciju, kas *izmet* solījumu, ja dati nav gatavi. Kad React mēģina renderēt komponentu, kas izsauc šādu funkciju, un dati joprojām tiek gaidīti, solījums tiek izmests. React tad 'pauzē' šī komponenta un tā bērnu renderēšanu, meklējot tuvāko <Suspense> robežu.

2. Suspense robeža (The Suspense Boundary)

<Suspense> komponents darbojas kā kļūdu robeža solījumiem. Tas pieņem fallback propu, kas ir UI, ko renderēt, kamēr kāds no tā bērniem (vai to pēctečiem) ir apturēts (t.i., izmet solījumu). Tiklīdz visi tā apakškokā izmestie solījumi atrisinās, fallback tiek aizstāts ar faktisko saturu.

Viena Suspense robeža var pārvaldīt vairākas asinhronas darbības. Piemēram, ja jums ir divi komponenti vienā <Suspense> robežā, un katram ir nepieciešams ielādēt datus, fallback tiks rādīts, līdz *abi* datu ielādes procesi būs pabeigti. Tas novērš daļēja UI parādīšanu un nodrošina koordinētāku ielādes pieredzi.

3. Kešatmiņas/Resursu pārvaldnieks (lietotāja atbildība)

Svarīgi ir tas, ka Suspense pats par sevi nenodarbojas ar datu ielādi vai kešošanu. Tas ir tikai koordinācijas mehānisms. Lai Suspense darbotos datu ielādei, jums ir nepieciešams slānis, kas:

Šis 'resursu pārvaldnieks' parasti tiek ieviests, izmantojot vienkāršu kešatmiņu (piemēram, Map vai objektu), lai uzglabātu katra resursa stāvokli (gaida, atrisināts vai kļūda). Lai gan jūs varat to izveidot manuāli demonstrācijas nolūkos, reālā lietotnē jūs izmantotu robustu datu ielādes bibliotēku, kas integrējas ar Suspense.

4. Vienlaicīgais režīms (React 18 uzlabojumi)

Lai gan Suspense var izmantot vecākās React versijās, tā pilnais spēks tiek atraisīts ar Concurrent React (iespējots pēc noklusējuma React 18 ar createRoot). Vienlaicīgais režīms ļauj React pārtraukt, pauzēt un atsākt renderēšanas darbu. Tas nozīmē:

Datu ielādes modeļi ar Suspense

Izpētīsim datu ielādes modeļu evolūciju ar Suspense parādīšanos.

1. modelis: Ielādē-tad-renderē (Fetch-Then-Render) (Tradicionāls ar Suspense ietvaru)

Šī ir klasiskā pieeja, kur dati tiek ielādēti, un tikai tad komponents tiek renderēts. Lai gan tas tieši neizmanto 'izmest solījumu' mehānismu datiem, jūs varat ietīt komponentu, kas *galu galā* renderē datus, Suspense robežā, lai nodrošinātu fallback. Tas vairāk ir par Suspense izmantošanu kā vispārēju ielādes UI orķestratoru komponentiem, kas galu galā kļūst gatavi, pat ja to iekšējā datu ielāde joprojām ir balstīta uz tradicionālo 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>Ielādē lietotāja detaļas...</p>;
  }

  return (
    <div>
      <h3>Lietotājs: {user.name}</h3>
      <p>E-pasts: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Fetch-Then-Render piemērs</h1>
      <Suspense fallback={<div>Notiek kopējā lapas ielāde...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

Plusi: Vienkārši saprotams, atpakaļsaderīgs. Var izmantot kā ātru veidu, kā pievienot globālu ielādes stāvokli.

Mīnusi: Neizslēdz šablona kodu UserDetails iekšienē. Joprojām pakļauts ūdenskritumiem, ja komponenti ielādē datus secīgi. Patiesībā neizmanto Suspense 'izmet-un-noķer' mehānismu pašiem datiem.

2. modelis: Renderē-tad-ielādē (Render-Then-Fetch) (Ielāde renderēšanas laikā, nav paredzēts produkcijai)

Šis modelis galvenokārt ir paredzēts, lai ilustrētu, ko nedarīt ar Suspense tieši, jo tas var novest pie bezgalīgām cilpām vai veiktspējas problēmām, ja to neapstrādā rūpīgi. Tas ietver mēģinājumu ielādēt datus vai izsaukt apturošu funkciju tieši komponenta renderēšanas fāzē, *bez* pienācīga kešošanas mehānisma.

// NELIETOJIET ŠO PRODUKCIJĀ BEZ PIENĀCĪGA KEŠOŠANAS SLĀŅA
// Tas ir tikai ilustrācijai, kā tieša 'izmešana' varētu konceptuāli darboties.

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; // Šeit Suspense sāk darboties
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>Lietotājs: {user.name}</h3>
      <p>E-pasts: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Render-Then-Fetch (Ilustratīvs, NAV ieteicams tieši)</h1>
      <Suspense fallback={<div>Ielādē lietotāju...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

Plusi: Parāda, kā komponents var tieši 'prasīt' datus un apturēt darbību, ja tie nav gatavi.

Mīnusi: Ļoti problemātiski produkcijai. Šī manuālā, globālā fetchedData un dataPromise sistēma ir vienkāršota, nespēj apstrādāt vairākus pieprasījumus, invalidāciju vai kļūdu stāvokļus robusti. Tā ir primitīva 'izmest-solījumu' koncepcijas ilustrācija, nevis modelis, ko pieņemt.

3. modelis: Ielādē-renderējot (Fetch-As-You-Render) (Ideālais Suspense modelis)

Šī ir paradigma, ko Suspense patiesi nodrošina datu ielādei. Tā vietā, lai gaidītu, kamēr komponents renderējas, pirms sākt datu ielādi, vai ielādēt visus datus iepriekš, Fetch-As-You-Render nozīmē, ka jūs sākat ielādēt datus *cik ātri vien iespējams*, bieži *pirms* vai *vienlaicīgi ar* renderēšanas procesu. Komponenti tad 'lasa' datus no kešatmiņas, un, ja dati nav gatavi, tie tiek apturēti. Galvenā ideja ir atdalīt datu ielādes loģiku no komponenta renderēšanas loģikas.

Lai ieviestu Fetch-As-You-Render, jums ir nepieciešams mehānisms, kas:

  1. Uzsāk datu ielādi ārpus komponenta renderēšanas funkcijas (piemēram, kad tiek atvērts maršruts vai noklikšķināta poga).
  2. Saglabā solījumu vai atrisinātos datus kešatmiņā.
  3. Nodrošina veidu, kā komponenti var 'lasīt' no šīs kešatmiņas. Ja dati vēl nav pieejami, lasīšanas funkcija izmet gaidošo solījumu.

Šis modelis risina ūdenskrituma problēmu. Ja diviem dažādiem komponentiem nepieciešami dati, to pieprasījumus var uzsākt paralēli, un UI parādīsies tikai tad, kad *abi* būs gatavi, ko orķestrē viena Suspense robeža.

Manuāla implementācija (izpratnei)

Lai saprastu pamatā esošo mehāniku, izveidosim vienkāršotu manuālu resursu pārvaldnieku. Reālā lietotnē jūs izmantotu specializētu bibliotēku.

import React, { Suspense } from 'react';

// --- Vienkāršs kešatmiņas/resursu pārvaldnieks --- //
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);
}

// --- Datu ielādes funkcijas --- //
const fetchUserById = (id) => {
  console.log(`Ielādē lietotāju ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'Alise Bērziņa', email: 'alice@example.com' },
      '2': { id: '2', name: 'Jānis Ozols', email: 'bob@example.com' },
      '3': { id: '3', name: 'Kārlis Līcis', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`Ielādē ierakstus lietotājam ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'Mans pirmais ieraksts' }, { id: 'p2', title: 'Ceļojumu piedzīvojumi' }],
      '2': [{ id: 'p3', title: 'Programmēšanas ieskati' }],
      '3': [{ id: 'p4', title: 'Globālās tendences' }, { id: 'p5', title: 'Vietējā virtuve' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Komponenti --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // Tas tiks apturēts, ja lietotāja dati nav gatavi

  return (
    <div>
      <h3>Lietotājs: {user.name}</h3>
      <p>E-pasts: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // Tas tiks apturēts, ja ierakstu dati nav gatavi

  return (
    <div>
      <h4>Lietotāja {userId} ieraksti:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>Ieraksti nav atrasti.</li>}
      </ul>
    </div>
  );
}

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

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

// Iepriekš ielādē dažus datus, pirms App komponents pat renderējas
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Fetch-As-You-Render ar Suspense</h1>
      <p>Šis demonstrē, kā datu ielāde var notikt paralēli, ko koordinē Suspense.</p>

      <Suspense fallback={<div>Ielādē lietotāja profilu un ierakstus...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Cita sadaļa</h2>
      <Suspense fallback={<div>Ielādē citu lietotāju...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

Šajā piemērā:

Bibliotēkas priekš Fetch-As-You-Render

Robustu resursu pārvaldnieka izveide un uzturēšana manuāli ir sarežģīta. Par laimi, vairākas nobriedušas datu ielādes bibliotēkas ir pieņēmušas vai pieņem Suspense, nodrošinot pārbaudītus risinājumus:

Šīs bibliotēkas abstrahē resursu izveides un pārvaldības sarežģītību, apstrādājot kešošanu, atkārtotu validāciju, optimistiskus atjauninājumus un kļūdu apstrādi, padarot Fetch-As-You-Render ieviešanu daudz vieglāku.

4. modelis: Iepriekšēja ielāde (Prefetching) ar Suspense-apzinošām bibliotēkām

Iepriekšēja ielāde ir spēcīga optimizācija, kurā jūs proaktīvi ielādējat datus, kas lietotājam, visticamāk, būs nepieciešami tuvākajā nākotnē, pirms viņš tos pat ir skaidri pieprasījis. Tas var krasi uzlabot uztverto veiktspēju.

Ar Suspense-apzinošām bibliotēkām iepriekšēja ielāde kļūst nemanāma. Jūs varat aktivizēt datu ielādi, reaģējot uz lietotāja mijiedarbībām, kas nekavējoties nemaina UI, piemēram, uzbraucot ar peli virs saites vai pogas.

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

// Pieņemsim, ka šie ir jūsu API izsaukumi
const fetchProductById = async (id) => {
  console.log(`Ielādē produktu ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Globālais sīkrīks X', price: 29.99, description: 'Universāls sīkrīks starptautiskai lietošanai.' },
      'B002': { id: 'B002', name: 'Universālā ierīce Y', price: 149.99, description: 'Modernākā ierīce, ko iecienījuši visā pasaulē.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Iespējot Suspense visiem vaicājumiem pēc noklusējuma
    },
  },
});

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) => {
    // Iepriekš ielādē datus, kad lietotājs uzbrauc ar peli virs produkta saites
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Iepriekš ielādē produktu ${productId}`);
  };

  return (
    <div>
      <h2>Pieejamie produkti:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Navigēt vai rādīt detaļas */ }}
          >Globālais sīkrīks X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Navigēt vai rādīt detaļas */ }}
          >Universālā ierīce Y (B002)</a>
        </li>
      </ul>
      <p>Uzbrauciet ar peli virs produkta saites, lai redzētu iepriekšēju ielādi darbībā. Atveriet tīkla cilni, lai novērotu.</p>
    </div>
  );
}

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

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Iepriekšēja ielāde ar React Suspense (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Rādīt Globālo sīkrīku X</button>
      <button onClick={() => setShowProductB(true)}>Rādīt Universālo ierīci Y</button>

      {showProductA && (
        <Suspense fallback={<p>Ielādē Globālo sīkrīku X...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Ielādē Universālo ierīci Y...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

Šajā piemērā, uzbraucot ar peli virs produkta saites, tiek aktivizēts `queryClient.prefetchQuery`, kas uzsāk datu ielādi fonā. Ja lietotājs pēc tam noklikšķina uz pogas, lai parādītu produkta detaļas, un dati jau ir kešatmiņā no iepriekšējās ielādes, komponents renderēsies uzreiz, neapturot darbību. Ja iepriekšējā ielāde joprojām notiek vai netika uzsākta, Suspense parādīs fallback, līdz dati būs gatavi.

Kļūdu apstrāde ar Suspense un kļūdu robežām (Error Boundaries)

Kamēr Suspense apstrādā 'ielādes' stāvokli, parādot fallback, tas tieši neapstrādā 'kļūdu' stāvokļus. Ja solījums, ko izmet apturošs komponents, tiek noraidīts (t.i., datu ielāde neizdodas), šī kļūda izplatīsies augšup pa komponentu koku. Lai graciozi apstrādātu šīs kļūdas un parādītu atbilstošu UI, jums jāizmanto kļūdu robežas (Error Boundaries).

Kļūdu robeža ir React komponents, kas implementē vai nu componentDidCatch, vai static getDerivedStateFromError dzīvescikla metodes. Tā notver JavaScript kļūdas jebkur savā bērnu komponentu kokā, ieskaitot kļūdas, ko izmet solījumi, kurus Suspense parasti notvertu, ja tie būtu gaidīšanas stāvoklī.

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

// --- Kļūdu robežas komponents --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Atjaunina stāvokli, lai nākamā renderēšana parādītu fallback UI.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Jūs varat arī reģistrēt kļūdu kļūdu ziņošanas servisā
    console.error("Notverta kļūda:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Jūs varat renderēt jebkuru pielāgotu fallback UI
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Kaut kas nogāja greizi!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Lūdzu, mēģiniet atsvaidzināt lapu vai sazinieties ar atbalsta dienestu.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Mēģināt vēlreiz</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Datu ielāde (ar kļūdas potenciālu) --- //
const fetchItemById = async (id) => {
  console.log(`Mēģina ielādēt vienumu ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Neizdevās ielādēt vienumu: tīkls nav sasniedzams vai vienums nav atrasts.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Piegādāts lēni', data: 'Šis vienums prasīja laiku, bet tika piegādāts!', status: 'success' });
    } else {
      resolve({ id, name: `Vienums ${id}`, data: `Dati vienumam ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // Demonstrācijas nolūkos atspējot atkārtošanu, lai kļūda būtu tūlītēja
    },
  },
});

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

  return (
    <div>
      <h3>Vienuma detaļas:</h3>
      <p>ID: {item.id}</p>
      <p>Nosaukums: {item.name}</p>
      <p>Dati: {item.data}</p>
    </div>
  );
}

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

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense un kļūdu robežas</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Ielādēt normālu vienumu</button>
        <button onClick={() => setFetchType('slow-item')}>Ielādēt lēnu vienumu</button>
        <button onClick={() => setFetchType('error-item')}>Ielādēt kļūdainu vienumu</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Ielādē vienumu ar Suspense...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

Ietverot savu Suspense robežu (vai komponentus, kas var tikt apturēti) kļūdu robežā, jūs nodrošināt, ka tīkla kļūmes vai servera kļūdas datu ielādes laikā tiek notvertas un apstrādātas graciozi, novēršot visas lietotnes avāriju. Tas nodrošina robustu un lietotājam draudzīgu pieredzi, ļaujot lietotājiem saprast problēmu un, iespējams, mēģināt vēlreiz.

Stāvokļa pārvaldība un datu invalidācija ar Suspense

Ir svarīgi precizēt, ka React Suspense galvenokārt risina asinhrono resursu sākotnējo ielādes stāvokli. Tas pēc būtības nepārvalda klienta puses kešatmiņu, nenodarbojas ar datu invalidāciju vai neorganizē mutācijas (izveides, atjaunināšanas, dzēšanas operācijas) un to sekojošos UI atjauninājumus.

Šeit Suspense-apzinošās datu ielādes bibliotēkas (React Query, SWR, Apollo Client, Relay) kļūst neaizstājamas. Tās papildina Suspense, nodrošinot:

Bez robustas datu ielādes bibliotēkas, šo funkciju implementēšana papildus manuālam Suspense resursu pārvaldniekam būtu nozīmīgs uzdevums, būtībā prasot jums izveidot savu datu ielādes ietvaru.

Praktiski apsvērumi un labākās prakses

Suspense pieņemšana datu ielādei ir nozīmīgs arhitektūras lēmums. Šeit ir daži praktiski apsvērumi globālai lietotnei:

1. Ne visiem datiem ir nepieciešams Suspense

Suspense ir ideāli piemērots kritiskiem datiem, kas tieši ietekmē komponenta sākotnējo renderēšanu. Nekritiskiem datiem, fona ielādēm vai datiem, ko var ielādēt slinki bez spēcīgas vizuālās ietekmes, tradicionālais useEffect vai iepriekšēja renderēšana joprojām var būt piemērota. Pārmērīga Suspense izmantošana var novest pie mazāk granulāras ielādes pieredzes, jo viena Suspense robeža gaida, līdz *visi* tās bērni atrisināsies.

2. Suspense robežu granularitāte

Rūpīgi izvietojiet savas <Suspense> robežas. Viena liela robeža lietotnes augšpusē varētu paslēpt visu lapu aiz ielādes indikatora, kas var būt nomācoši. Mazākas, granulārākas robežas ļauj dažādām lapas daļām ielādēties neatkarīgi, nodrošinot progresīvāku un atsaucīgāku pieredzi. Piemēram, robeža ap lietotāja profila komponentu un cita ap ieteikto produktu sarakstu.

<div>
  <h1>Produkta lapa</h1>
  <Suspense fallback={<p>Ielādē galvenās produkta detaļas...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>Saistītie produkti</h2>
  <Suspense fallback={<p>Ielādē saistītos produktus...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

Šī pieeja nozīmē, ka lietotāji var redzēt galvenās produkta detaļas, pat ja saistītie produkti joprojām tiek ielādēti.

3. Servera puses renderēšana (SSR) un straumēšanas HTML

React 18 jaunās straumēšanas SSR API (renderToPipeableStream) pilnībā integrējas ar Suspense. Tas ļauj jūsu serverim nosūtīt HTML, tiklīdz tas ir gatavs, pat ja daļa lapas (piemēram, no datiem atkarīgi komponenti) joprojām tiek ielādēta. Serveris var straumēt vietturi (no Suspense fallback) un pēc tam straumēt faktisko saturu, kad dati atrisinās, neprasot pilnīgu klienta puses atkārtotu renderēšanu. Tas ievērojami uzlabo uztverto ielādes veiktspēju globāliem lietotājiem ar dažādiem tīkla apstākļiem.

4. Pakāpeniska ieviešana

Jums nav jāpārraksta visa lietotne, lai izmantotu Suspense. Jūs varat to ieviest pakāpeniski, sākot ar jaunām funkcijām vai komponentiem, kas visvairāk gūtu labumu no tā deklaratīvajiem ielādes modeļiem.

5. Rīki un atkļūdošana

Lai gan Suspense vienkāršo komponentu loģiku, atkļūdošana var būt atšķirīga. React DevTools sniedz ieskatu Suspense robežās un to stāvokļos. Iepazīstieties ar to, kā jūsu izvēlētā datu ielādes bibliotēka atklāj savu iekšējo stāvokli (piemēram, React Query Devtools).

6. Laika ierobežojumi Suspense fallbackiem

Ļoti ilgu ielādes laiku gadījumā jūs varētu vēlēties ieviest laika ierobežojumu savam Suspense fallback vai pēc noteikta laika pārslēgties uz detalizētāku ielādes indikatoru. useDeferredValue un useTransition hooki React 18 var palīdzēt pārvaldīt šos niansētākos ielādes stāvokļus, ļaujot parādīt 'veco' UI versiju, kamēr tiek ielādēti jauni dati, vai atlikt nesteidzamus atjauninājumus.

Datu ielādes nākotne React: React Servera Komponenti un tālāk

Datu ielādes ceļojums React neapstājas pie klienta puses Suspense. React Servera Komponenti (RSC) ir nozīmīga evolūcija, kas sola izpludināt robežas starp klientu un serveri un vēl vairāk optimizēt datu ielādi.

React turpinot attīstīties, Suspense būs arvien centrālāka daļa puzlē, veidojot augstas veiktspējas, lietotājam draudzīgas un uzturamas lietotnes. Tas mudina izstrādātājus virzīties uz deklaratīvāku un noturīgāku veidu, kā apstrādāt asinhronas darbības, pārceļot sarežģītību no atsevišķiem komponentiem uz labi pārvaldītu datu slāni.

Nobeigums

React Suspense, sākotnēji funkcija koda sadalīšanai, ir izplaucis par pārveidojošu rīku datu ielādei. Pieņemot Fetch-As-You-Render modeli un izmantojot Suspense-apzinošas bibliotēkas, izstrādātāji var ievērojami uzlabot savu lietotņu lietotāja pieredzi, novēršot ielādes ūdenskritumus, vienkāršojot komponentu loģiku un nodrošinot vienmērīgus, koordinētus ielādes stāvokļus. Apvienojumā ar kļūdu robežām robustai kļūdu apstrādei un React Servera Komponentu nākotnes solījumu, Suspense dod mums iespēju veidot lietotnes, kas ir ne tikai veiktspējīgas un noturīgas, bet arī pēc būtības patīkamākas lietotājiem visā pasaulē. Pāreja uz Suspense virzītu datu ielādes paradigmu prasa konceptuālu pielāgošanos, bet ieguvumi koda skaidrības, veiktspējas un lietotāju apmierinātības ziņā ir būtiski un ir vērts ieguldījumu.

React Suspense resursu ielāde: Mūsdienu datu ielādes modeļu apgūšana | MLOG