Lietuvių

Įvaldykite React Suspense duomenų gavimui. Išmokite deklaratyviai valdyti įkėlimo būsenas, gerinti UX su perėjimais ir apdoroti klaidas su Error Boundaries.

React Suspense ribos: išsami deklaratyvaus įkėlimo būsenų valdymo analizė

Šiuolaikinio žiniatinklio kūrimo pasaulyje nepriekaištingos ir greitai reaguojančios vartotojo patirties sukūrimas yra svarbiausias tikslas. Vienas iš nuolatinių iššūkių, su kuriais susiduria programuotojai, yra įkėlimo būsenų valdymas. Nuo duomenų gavimo vartotojo profiliui iki naujos programos skilties įkėlimo – laukimo akimirkos yra kritinės. Istoriškai tai buvo susiję su sudėtingu loginių (boolean) žymų, tokių kaip isLoading, isFetching ir hasError, tinklu, išmėtytu po visus mūsų komponentus. Šis imperatyvus požiūris apkrauna mūsų kodą, komplikuoja logiką ir yra dažnas klaidų, tokių kaip lenktynių sąlygos (race conditions), šaltinis.

Pristatome „React Suspense“. Iš pradžių pristatytas kodo skaidymui su React.lazy(), jo galimybės su „React 18“ smarkiai išsiplėtė ir tapo galingu, aukščiausio lygio mechanizmu asinchroninėms operacijoms, ypač duomenų gavimui, valdyti. „Suspense“ leidžia mums valdyti įkėlimo būsenas deklaratyviai, iš esmės keičiant tai, kaip rašome ir mąstome apie savo komponentus. Užuot klausę „Ar aš įkeliu?“, mūsų komponentai gali tiesiog pasakyti: „Man reikia šių duomenų, kad galėčiau atvaizduoti. Kol laukiu, prašau, parodykite šį atsarginį UI.“

Šis išsamus vadovas nuves jus į kelionę nuo tradicinių būsenos valdymo metodų iki deklaratyvios „React Suspense“ paradigmos. Mes išnagrinėsime, kas yra „Suspense“ ribos, kaip jos veikia tiek kodo skaidymui, tiek duomenų gavimui, ir kaip organizuoti sudėtingus įkėlimo UI, kurie džiugintų jūsų vartotojus, o ne juos vargintų.

Senasis būdas: varginantis rankinis įkėlimo būsenų valdymas

Prieš pilnai įvertinant „Suspense“ eleganciją, būtina suprasti problemą, kurią jis sprendžia. Pažvelkime į tipišką komponentą, kuris gauna duomenis naudodamas useEffect ir useState kabliukus (hooks).

Įsivaizduokite komponentą, kuris turi gauti ir parodyti vartotojo duomenis:


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(() => {
    // Atstatome būseną naujam userId
    setIsLoading(true);
    setUser(null);
    setError(null);

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

    fetchUser();
  }, [userId]); // Iš naujo gauname duomenis, kai pasikeičia userId

  if (isLoading) {
    return <p>Įkeliamas profilis...</p>;
  }

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

  return (
    <div>
      <h1>{user.name}</h1>
      <p>El. paštas: {user.email}</p>
    </div>
  );
}

Šis modelis yra funkcionalus, tačiau turi keletą trūkumų:

Pristatome „React Suspense“: paradigmos pokytis

„Suspense“ apverčia šį modelį aukštyn kojomis. Užuot komponentui valdant įkėlimo būseną viduje, jis tiesiogiai praneša „React“ apie savo priklausomybę nuo asinchroninės operacijos. Jei duomenys, kurių jam reikia, dar nėra pasiekiami, komponentas „sustabdo“ atvaizdavimą.

Kai komponentas sustabdomas, „React“ kyla aukštyn komponentų medžiu, ieškodamas artimiausios „Suspense“ ribos. „Suspense“ riba yra jūsų medyje apibrėžtas komponentas, naudojant <Suspense>. Ši riba tada atvaizduos atsarginį UI (pvz., suktuką ar karkasinį įkėlėją), kol visi jame esantys komponentai išspręs savo duomenų priklausomybes.

Pagrindinė idėja yra sujungti duomenų priklausomybę su komponentu, kuriam jos reikia, tuo pačiu centralizuojant įkėlimo UI aukštesniame komponentų medžio lygmenyje. Tai išvalo komponentų logiką ir suteikia jums galingą kontrolę vartotojo įkėlimo patirčiai.

Kaip komponentas „sustabdo“ vykdymą?

„Suspense“ magija slypi modelyje, kuris iš pradžių gali atrodyti neįprastas: „Promise“ išmetimas. Su „Suspense“ suderinamas duomenų šaltinis veikia taip:

  1. Kai komponentas prašo duomenų, duomenų šaltinis patikrina, ar jis turi duomenis podėlyje (cache).
  2. Jei duomenys yra prieinami, jis juos grąžina sinchroniškai.
  3. Jei duomenys nėra prieinami (t. y., jie šiuo metu gaunami), duomenų šaltinis išmeta „Promise“, kuris atspindi vykstantį gavimo prašymą.

„React“ pagauna šį išmestą „Promise“. Tai nesugadina jūsų programos. Vietoj to, jis tai interpretuoja kaip signalą: „Šis komponentas dar nėra pasirengęs atvaizduoti. Pristabdykite jį ir ieškokite virš jo esančios „Suspense“ ribos, kad parodytumėte atsarginį UI.“ Kai „Promise“ bus įvykdytas, „React“ bandys iš naujo atvaizduoti komponentą, kuris dabar gaus savo duomenis ir sėkmingai atvaizduos.

<Suspense> riba: jūsų deklaratyvus įkėlimo UI

<Suspense> komponentas yra šio modelio širdis. Jį naudoti neįtikėtinai paprasta, jis priima vieną privalomą parametrą: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Mano programa</h1>
      <Suspense fallback={<p>Įkeliamas turinys...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

Šiame pavyzdyje, jei SomeComponentThatFetchesData sustabdys vykdymą, vartotojas matys pranešimą „Įkeliamas turinys...“, kol duomenys bus paruošti. Atsarginis elementas (fallback) gali būti bet koks galiojantis „React“ mazgas, nuo paprastos eilutės iki sudėtingo karkasinio komponento.

Klasikinis panaudojimo atvejis: kodo skaidymas su React.lazy()

Labiausiai įsitvirtinęs „Suspense“ panaudojimas yra kodo skaidymas. Tai leidžia atidėti komponento „JavaScript“ kodo įkėlimą, kol jis iš tikrųjų bus reikalingas.


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

// Šio komponento kodas nebus pradiniame pakete.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Turinys, kuris įkeliamas iš karto</h2>
      <Suspense fallback={<div>Įkeliamas komponentas...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Čia „React“ gaus „JavaScript“ kodą HeavyComponent komponentui tik tada, kai pirmą kartą bandys jį atvaizduoti. Kol jis bus gaunamas ir analizuojamas, bus rodomas „Suspense“ atsarginis elementas. Tai yra galinga technika, skirta pagerinti pradinio puslapio įkėlimo laiką.

Šiuolaikinė riba: duomenų gavimas su „Suspense“

Nors „React“ suteikia „Suspense“ mechanizmą, jis nepateikia konkretaus duomenų gavimo kliento. Norint naudoti „Suspense“ duomenų gavimui, jums reikia duomenų šaltinio, kuris su juo integruojasi (t. y., kuris išmeta „Promise“, kai duomenys laukiami).

Tokios sistemos kaip „Relay“ ir „Next.js“ turi integruotą, aukščiausio lygio palaikymą „Suspense“. Populiarios duomenų gavimo bibliotekos, tokios kaip „TanStack Query“ (buvusi „React Query“) ir „SWR“, taip pat siūlo eksperimentinį arba pilną palaikymą.

Kad suprastumėte koncepciją, sukurkime labai paprastą, konceptualų apvalkalą aplink fetch API, kad jis būtų suderinamas su „Suspense“. Pastaba: tai supaprastintas pavyzdys edukaciniais tikslais ir nėra paruoštas produkcijai. Jam trūksta tinkamo podėlio valdymo ir klaidų apdorojimo subtilybių.


// data-fetcher.js
// Paprasta podėlio (cache) sistema rezultatams saugoti
const cache = new Map();

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

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Čia ir yra magija!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Duomenų gavimas nepavyko, būsena ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Šis apvalkalas palaiko paprastą kiekvieno URL būseną. Kai iškviečiama fetchData, ji patikrina būseną. Jei ji yra laukiama, ji išmeta „promise“. Jei ji sėkminga, ji grąžina duomenis. Dabar perrašykime mūsų UserProfile komponentą, naudodami tai.


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

// Komponentas, kuris realiai naudoja duomenis
function ProfileDetails({ userId }) {
  // Bandoma nuskaityti duomenis. Jei jie neparuošti, vykdymas bus sustabdytas.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>El. paštas: {user.email}</p>
    </div>
  );
}

// Tėvinis komponentas, kuris apibrėžia įkėlimo būsenos UI
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Įkeliamas profilis...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Pažiūrėkite į skirtumą! ProfileDetails komponentas yra švarus ir sutelktas tik į duomenų atvaizdavimą. Jame nėra isLoading ar error būsenų. Jis tiesiog prašo duomenų, kurių jam reikia. Atsakomybė rodyti įkėlimo indikatorių buvo perkelta į tėvinį komponentą UserProfile, kuris deklaratyviai nurodo, ką rodyti laukiant.

Sudėtingų įkėlimo būsenų organizavimas

Tikroji „Suspense“ galia atsiskleidžia, kai kuriate sudėtingus UI su keliomis asinchroninėmis priklausomybėmis.

Įdėtosios „Suspense“ ribos laipsniškam UI atvaizdavimui

Galite dėti „Suspense“ ribas vieną į kitą, kad sukurtumėte tobulesnę įkėlimo patirtį. Įsivaizduokite prietaisų skydelio puslapį su šonine juosta, pagrindine turinio sritimi ir naujausių veiklų sąrašu. Kiekvienam iš jų gali prireikti savo duomenų gavimo.


function DashboardPage() {
  return (
    <div>
      <h1>Prietaisų skydelis</h1>
      <div className="layout">
        <Suspense fallback={<p>Įkeliama navigacija...</p>}>
          <Sidebar />
        </Suspense>

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

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

Su šia struktūra:

Tai leidžia kuo greičiau parodyti vartotojui naudingą turinį, dramatiškai pagerinant suvokiamą našumą.

Vengiame UI „spragsėjimo“ efekto

Kartais laipsniškas požiūris gali sukelti erzinantį efektą, kai keli suktukai atsiranda ir dingsta greita seka, efektas dažnai vadinamas „spragsėjimu“ (popcorning). Norėdami tai išspręsti, galite perkelti „Suspense“ ribą aukščiau medyje.


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

Šioje versijoje vienas DashboardSkeleton rodomas tol, kol visi vaikiniai komponentai (Sidebar, MainContent, ActivityFeed) turės paruoštus duomenis. Tada visas prietaisų skydelis pasirodo vienu metu. Pasirinkimas tarp įdėtųjų ribų ir vienos aukštesnio lygio ribos yra UX dizaino sprendimas, kurį „Suspense“ leidžia įgyvendinti trivialiai.

Klaidų apdorojimas su klaidų ribomis (Error Boundaries)

„Suspense“ valdo laukiama (pending) „promise“ būseną, bet kaip dėl atmesta (rejected) būsenos? Jei komponento išmestas „promise“ atmetamas (pvz., dėl tinklo klaidos), tai bus traktuojama kaip bet kuri kita atvaizdavimo klaida „React“ programoje.

Sprendimas yra naudoti klaidų ribas (Error Boundaries). Klaidų riba yra klasės komponentas, kuris apibrėžia specialų gyvavimo ciklo metodą, componentDidCatch() arba statinį metodą getDerivedStateFromError(). Jis pagauna „JavaScript“ klaidas bet kurioje savo vaikinių komponentų medžio vietoje, registruoja tas klaidas ir rodo atsarginį UI.

Štai paprastas „Error Boundary“ komponentas:


import React from 'react';

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

  static getDerivedStateFromError(error) {
    // Atnaujiname būseną, kad kitas atvaizdavimas parodytų atsarginį UI.
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // Taip pat galite registruoti klaidą klaidų pranešimo tarnyboje
    console.error("Pagauta klaida:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Galite atvaizduoti bet kokį pasirinktinį atsarginį UI
      return <h1>Kažkas nutiko negerai. Bandykite dar kartą.</h1>;
    }

    return this.props.children; 
  }
}

Tada galite derinti klaidų ribas su „Suspense“, kad sukurtumėte patikimą sistemą, kuri valdo visas tris būsenas: laukiama, sėkminga ir klaida.


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

function App() {
  return (
    <div>
      <h2>Vartotojo informacija</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Įkeliama...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Naudojant šį modelį, jei duomenų gavimas UserProfile viduje pavyksta, rodomas profilis. Jei jis laukiamas, rodomas „Suspense“ atsarginis elementas. Jei jis nepavyksta, rodomas klaidų ribos atsarginis elementas. Logika yra deklaratyvi, kompozicinė ir lengvai suprantama.

Perėjimai (Transitions): raktas į neblokuojančius UI atnaujinimus

Yra dar viena paskutinė dėlionės dalis. Apsvarstykite vartotojo sąveiką, kuri sukelia naują duomenų gavimą, pavyzdžiui, paspaudus mygtuką „Kitas“, norint peržiūrėti kitą vartotojo profilį. Su aukščiau aprašyta konfigūracija, tą akimirką, kai paspaudžiamas mygtukas ir pasikeičia userId parametras, UserProfile komponentas vėl bus sustabdytas. Tai reiškia, kad šiuo metu matomas profilis išnyks ir bus pakeistas įkėlimo atsarginiu elementu. Tai gali atrodyti staiga ir trikdančiai.

Štai kur pasirodo perėjimai (transitions). Perėjimai yra nauja „React 18“ funkcija, leidžianti pažymėti tam tikrus būsenos atnaujinimus kaip neskubius. Kai būsenos atnaujinimas yra apgaubtas perėjimu, „React“ toliau rodys seną UI (pasenusį turinį), kol fone ruoš naują turinį. Jis atliks UI atnaujinimą tik tada, kai naujas turinys bus paruoštas rodymui.

Pagrindinė API tam yra useTransition kabliukas.


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

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

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

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Kitas vartotojas
      </button>

      {isPending && <span> Įkeliamas naujas profilis...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Įkeliamas pradinis profilis...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Štai kas vyksta dabar:

  1. Įkeliamas pradinis profilis su userId: 1, rodomas „Suspense“ atsarginis elementas.
  2. Vartotojas paspaudžia „Kitas vartotojas“.
  3. setUserId iškvietimas yra apgaubtas startTransition.
  4. „React“ pradeda atmintyje atvaizduoti UserProfile su nauju userId, lygiu 2. Tai sukelia jo sustabdymą.
  5. Svarbiausia, užuot rodęs „Suspense“ atsarginį elementą, „React“ toliau ekrane rodo seną UI (vartotojo 1 profilį).
  6. isPending loginė reikšmė, grąžinta iš useTransition, tampa true, leidžiant mums rodyti subtilų, įterptą įkėlimo indikatorių, neišmontuojant seno turinio.
  7. Kai tik duomenys vartotojui 2 yra gauti ir UserProfile gali sėkmingai atvaizduoti, „React“ patvirtina atnaujinimą, ir naujas profilis sklandžiai pasirodo.

Perėjimai suteikia galutinį kontrolės lygį, leidžiantį kurti sudėtingas ir vartotojui draugiškas įkėlimo patirtis, kurios niekada neatrodo trikdančios.

Geriausios praktikos ir bendri aspektai

Išvada

„React Suspense“ yra daugiau nei tik nauja funkcija; tai esminė evoliucija, kaip mes artėjame prie asinchroniškumo „React“ programose. Atsisakydami rankinių, imperatyvių įkėlimo žymų ir priimdami deklaratyvų modelį, galime rašyti komponentus, kurie yra švaresni, atsparesni ir lengviau komponuojami.

Derindami <Suspense> laukiančioms būsenoms, klaidų ribas nesėkmės būsenoms ir useTransition sklandiems atnaujinimams, turite pilną ir galingą įrankių rinkinį. Galite organizuoti viską nuo paprastų įkėlimo suktukų iki sudėtingų, laipsniškų prietaisų skydelio atvaizdavimų su minimaliu, nuspėjamu kodu. Pradėję integruoti „Suspense“ į savo projektus, pamatysite, kad tai ne tik pagerina jūsų programos našumą ir vartotojo patirtį, bet ir dramatiškai supaprastina jūsų būsenos valdymo logiką, leidžiant jums sutelkti dėmesį į tai, kas iš tikrųjų svarbu: kurti puikias funkcijas.