Edument

Skapa effektiva användarupplevelser med React Suspense - en djupdykning

Du får en introduktion till React Suspense som ingår i Reacts kommande Concurrent Mode, en ny uppsättning funktioner som låter utvecklare deklarativt invänta, att komponenter och deras resursberoenden - data, kod, bilder och annat innehåll - laddas in asynkront innan rendering sker. Suspense möjliggör komplexa laddningssekvenser och visuella övergångar i applikationer byggda med React, vilket förbättrar både utvecklar- och användarupplevelsen.

Suspense och Concurrent Mode är i skrivande stund experimentell funktionalitet (väntas dock släppas publikt inom kort) men tredje parts mjukvara har nyligen inkorporerat stöd för det, däribland react-query, vilket kommer användas för hämtning av data i den exempelapplikation som denna text åskådliggör.


Koden för applikationen finns att tillgå på Github, i mappen src/ex05.



Översikt av Suspense

Suspense släpptes redan i React 16.6 för ett specifikt scenario - splittning av kod (“code splitting”) och fördröjd inladdning (“lazy-loading”) av komponenter:

// listing 1.
import React, {
  lazy,
  Suspense,
} from 'react';

// lazy-load the Home component upon rendering.
const Home = lazy(() => import('./Home'));

function App() {
  return (
    <Suspense fallback={'Loading...'}>
      <Home />
    </Suspense>
  )
}


Följande sker i kodsnutten ovan:

  1. Komponenten App försöker rendera den kodsplittade komponenten Home, som avgränsas av en s.k. Suspense-barriär (“Suspense boundary”).

  2. Eftersom koden för Home laddas in endast då den begärs, d.v.s. första gången komponenten renderas, gör React ett nätverksanrop för att hämta dess kodfil. Det Promise objekt som returneras från anropet (genom import()) kommer att propagera och fångas upp Suspense-barriären.

  3. Suspense-barriären inväntar Promise objektet (= nätverksanropets svar) och visar en temporär komponent (via fallback prop) under tiden.

  4. När komponentens kodfil slutligen har hämtats renderar Suspense-barriären om Home.


Detta förlopp innebär att renderingen av Home pausas (“suspends”) under det att dess resursberoende - kodfilen - hämtas och återupptas senare.


Med hjälp av promises kan således tredje parts kod, exempelvis biblioteket react-query för datahämtning, använda en standardmekanism för att kommunicera med React om de resurser, som en komponent behöver, är tillgängliga eller ej.



Hämtning av data

Med Concurrent Mode har Suspense utökats till att låta en applikation invänta hämtning av ej endast kod men även andra typer av resurser, såsom data, bilder och dokument. Detta avsnitt tittar närmare på dessa scenarion och utforskar Suspense API:et via en exempelapplikation.


Laddningssekvenser

Exempelapplikationen låter en välja en Githubanvändare från en lista och visa en vy med associerad profildata - sammanfattning (namn och avatarbild), repositories och följare.

Varje kodsnutt (“listing”) nedan refererar till den källfil på Github där koden är hämtad från.

Notera att vissa kodstycken som återfinns i källfilerna har utelämnats i de kodsnuttar som följer, för tydlighets skull.


När en Githubanvändare väljs (klickas på) renderas innehållet för profilvyn:

// listing 2: src/ex05/App.js

<Suspense fallback={`Loading ${currentUser}...`}>
  <User user={currentUser} />

  <Suspense fallback={`Loading repos...`}>
    <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} />           
  </Suspense>

  <Suspense fallback={`Loading followers...`}>
    <Followers endpoint={`${BASE_URL}/${currentUser}/followers`} />
  </Suspense>
</Suspense>


Observera att flera Suspense-barriärer har deklarerats, en yttre och två inre. Detta förklaras närmare i avsnittet Nästlade Suspense-barriärer nedan.

Tre komponenter - User, Repos and Followers - kan pausas medan de väntar på sina respektive resursberoenden. En närmare titt på User:

// listing 3: src/ex05/User.js

function User({ user }) {
  // upon first render, this throws a promise and stops further execution, as the component suspends.
  const data = useFetch(`${BASE_URL}/${user}`);

  // this code is reached upon second render.
  const {
    avatar_url,
    name
  } = data;

  return (
    <>
      <Img src={avatar_url} />
      <h1>{name}</h1>
    </>
  );
}


Den egna hook:en useFetch är enbart en tunn “wrapper” kring hook:en useQuery som react-query tillhandahåller.


Vid den initiala renderingen av User är profilinfo (namn och avatar URL) ännu ej tillgänglig och måste hämtas. useFetch (som i sin tur anropar useQuery) startar ett nätverksanrop och det resulterande Promise objektet fångas av upp den yttre Suspense-barriären (se kodsnutt 2). Fallback-komponenten (en laddningsindikator) renderas på sidan istället och User är nu pausad medan profilinfo inväntas.


User är pausad fortsätter React att rendera Repos och Followers, som även de behöver hämta och invänta data. De pausas båda i sina respektive Suspense-barriärer.


När profilinfo är tillgänglig renderar React User åter och visar dess innehåll. Vid dennaa tidpunkt kan Repos och / eller Followers befinna sig i ett pausat läge, i vilket fall deras Fallback-komponenter visas. Om deras databeroenden däremot är uppfyllda innebär det att hela profilvyn nu är komplett och fullt renderad.

När väl data för en komponent har hämtats en gång cachas denna för framtida nyttjande.

I nuvarande exempel betyder det att då all data för en given (vald) användare - profilinfo, repositories och följare - har hämtats, sparas och returneras denna vid alla kommande tillfällen då denna användare ska visas, och komponentera kommer ej behöva pausas. Detta kan ändras, t.ex. om cachad data behöver förnyas och därmed hämtas om.


Nästlade Suspense-barriärer

Den sista paragrafen ovan rymmer en viktig insikt; genom att nästla Suspense-barriärer på detta sätt kan utvecklaren enkelt orkestrera komponenters laddningssekvenser, d.v.s. avgöra hur och när dessa visas, på ett för applikationen optimalt sätt vad gäller användarupplevelsen.


I exempelapplikationen utgör Githubanvändarens profilinfo (namn och avatarbild) det kritiska innehållet, som ska visas så fort som möjligt, medan övrig data - repositories samt följare - inte har lika hög prioritet (och kan potentiellt även ta längre tid att ladda in). En jämförelse kan göras med en omskrivning av kodsnutt 2 utan inre Suspense-barriärer:

// listing 4.
<Suspense fallback={<Fallback />}>
  <User user={currentUser} />
  <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} />
  <Followers endpoint={`${BASE_URL}/${currentUser}/followers`} />
</Suspense>


Med denna variant kommer React (och den yttre samt nu enda) Suspense-barriären pausa alla tre komponenter och invänta deras databeroenden innan rendering återupptas, vilket därmed förhindrar att profilinfo kan visas så snart den är tillgänglig. Denna prioritering kan dock, som synes, enkelt åstadkommas genom att wrappa komponenter i egna Suspense-barriärer.


Hämtning av bilder

Komponenten User i kodsnutt 3 renderar en Githubanvändares avatar via en egen Img komponent, istället för att direkt rendera ett <img src={avatar_url}> element. Ett applikationskrav är att Githubanvändarens namn och avatarbild måste hämtas och visas tillsammans som en “enhet”; avatarbilden måste således också betraktas som en resurs User är beroende av och skall invänta; denna komponent kommer alltså pausas tills dess att både profilinfo och avatarbild har laddats in.

useQuery i react-query upplåter till utvecklaren att själv tillhandahålla en funktion som utför den faktiska datahämtningen och returnerar ett Promise objekt; i exempelapplikationen använda biblioteket Axios för komponentdata:

// listing 5: src/ex05/fetch.js

async function doFetch(uri) {
  const result = await axios(uri)
  return result.data;
}


Hämtning av en bild kräver ett särskilt tillvägagångssätt:

// listing 6: src/ex05/fetch.js

function doImageFetch(src) {
  return new Promise(resolve => {
    const img = document.createElement('img');
    img.src = src;
    img.onload = () => {
      resolve(src)
    }
  });
}


För att rekapitulera, oavsett vilken resurs som ska hämtas är det via promises som komponenter kommunicerar med React när de bör pausas och sedermera återuppta rendering.



Visuella övergångar

Koden för att rendera listan med Githubanvändare ses i kodsnutt 7:

// listing 7: src/ex05/App.js

users.map(user => (
  <a
    key={user}
    onClick={() => selectUser(user)}
  >
    {/* display pending indicator if loading currentUser. */}
    {user} {(isPending && currentUser === user) && '...'}
  </a>
))


Då en användare väljs uppdateras en tillståndsvariabel (“state variable”) som avser den valda Githubanvändaren (“currentUser”), vilket i sin tur möjliggör att en pågående (“pending”) indikator kan visas bredvid denne. Notera flaggan isPending; var härstammar denna från?


Om exempelapplikationen körs i webbläsaren kan en observera att profilvyn inte visas omedelbart då en användare väljs (vilket direkt skulle visa Fallback-komponenten för den yttre Suspense-barriären då profildata inväntas). Denna övergång (“transition”) från vyn med användarlistan (kodsnutt 7) till profilvyn (kodsnutt 2) är alltså fördröjd.


Förmågan att förbli i ett visuellt tillstånd under en kort tidsperiod innan en övergång sker till ett annat tillstånd ger en förbättrad användarupplevelse, då React kan förbereda nästa tillstånd “i bakgrunden” och enkelt byta till det då all data har hämtats och renderingen är klar, utan att behöva visa oönskade och ofta irriterande laddningsspinners eller andra indikatorer.


En övergång implementeras enligt följande kod:

// listing 8: src/ex05/App.js

const [currentUser, setCurrentUser] = useState(null);
const [showProfile, setShowProfile] = useState(false);

const [startTransition, isPending] = useTransition({
  timeoutMs: 5000
});

const selectUser = user => {
  setCurrentUser(user);

  // initiate a "transition", i.e. prepare for switching to the profile view, in the background.
  startTransition(() => {
    // start to fetch user profile information.
    preFetch(`${BASE_URL}/${user}`);

    // perform a low-priority state update to begin rendering the profile view, as part of the background work.
    setShowProfile(true);
  })
};


Då en användare i listan väljs körs funktionen selectUser och

  • Tillståndsvariabeln currentUser uppdateras och triggar en omrendering av den aktuella komponenten (App).

  • En övergång påbörjas med funktionen startTransition, som returnerats from den i React inbyggda hook:en useTransition.


En övergång låter utvecklaren specificera en s.k. lågprioriterad tillståndsuppdatering (“low-priority state update”), där React börjar rendera nästa visuella tillstånd i minnet, som bakgrundsarbete. React ger flaggan isPending för att signalera att detta arbete pågår.


I kodsnutt 8 utgörs den lågprioriterade tillståndsuppdateringen av att flippa flaggan showProfile, som då den är satt till true antyder att profilvyn - nästa visuella tillstånd - ska renderas.

Förmågan att utföra multipla tillståndsuppdateringar samtidigt är en central aspekt i Concurrent Mode; se översiktskapitlet för mer information.


I övergången förhämtas profilinfo (namn och avatar URL), s.k. “prefetching”; mer om detta i avsnittet Förhämtning (“prefetching)” av data nedan.


Då tillståndsvariabeln currentUser är satt till att reflektera den valda Githubanvändaren och isPending är true eftersom en övergång förbereds i bakgrunden, visas nu en “pending” indikator, enligt koden i kodsnutt 7.


När tillståndsvariabeln showProfile uppdateras i anropet till startTransition instrueras React att rendera om den aktuella komponenten (App, som återges nedan i sin helhet, kodsnuttarna 2, 7 och 8 kombinerade) i bakgrunden:

// listing 9: src/ex05/App.js

function App() {
  const [currentUser, setCurrentUser] = useState(null);
  const [showProfile, setShowProfile] = useState(false);

  const [startTransition, isPending] = useTransition({
  timeoutMs: 5000
  });

  const selectUser = user => {
    setCurrentUser(user);

    // initiate a "transition", i.e. prepare for switching to the profile view, in the background.
    startTransition(() => {
      // start to fetch user profile information.
      preFetch(`${BASE_URL}/${user}`);

      // perform a low-priority state update to begin rendering the profile view, as part of the background work.
      setShowProfile(true);
    })
  };

  // create a fallback component for the outer Suspense boundary; when not rendering the profile view, set it to null, as rendering the list of users doesn't suspend.
  const Fallback = () => showProfile
    ? `Loading ${currentUser}...`
    : null;

  return (
    <Suspense fallback={<Fallback />}>    
      {showProfile
        ? (
          <>
            <User user={currentUser} />
            
            <Suspense fallback={`Loading repos...`}>
              <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} />
            </Suspense>

            <Suspense fallback={`Loading followers...`}>
              <Followers endpoint={`${BASE_URL}/${currentUser}/followers`} />
            </Suspense>
          </>
        )
        : users.map(user => (
          <a
            key={user}
            onClick={() => selectUser(user)}
          >
            {user} {(isPending && currentUser === user) && '...'}
          </a>
        ))}
    </Suspense>
  )
}


Eftersom showProfile är true ser React till att rendera profilvyn, med dess komponenter och Suspense-barriärer, såsom beskrivits tidigare. Notera åter att allt detta sker i bakgrunden.


Läs mer om övergångar i React-dokumentationen:


Tidsgräns (“timeout”) för övergångar

Om profilvyn som renderas i bakgrunden blir färdig inom en kort tidsrymd byter React direkt till den. Om denna process dröjer, exempelvis för att User komponentens data (profilinfo) hämtas över ett långsamt nätverk, kan övergången nå en förbestämd tidgräns och istället visa den yttre Suspense-barriärens fallback. Tidsgränsen anges i millisekunder som parameter till useTransition; i kodsnutt 9 är denna satt till 5000 ms.


Förhämtning (“prefetching)” av data

Det nämndes tidigare att då övergången sker för att visa profilvyn förhämtas den data som krävs för att rendera User, d.v.s. nätverksförfrågan för att ladda profilinfo skickas redan innan bakgrundsrenderingen av nästa visuella tillstånd ens startar.


Förhämtning är möjligt i detta fall eftersom Githubanvändarens API URL är känd på förhand. Idealt kan således profilinfo redan vara tillgänglig när User renderas första gången och därmed behöver komponenten ej pausas av React.


Betänk ett scenario där det räcker att slutanvändarens markör hovrar över en Githubanvändare i listan för att initiera hämtning av associerad data; om denna Githubanvändare sedan faktiskt väljs för att visa profilvyn kommer all data sannolikt vara tillgänglig och övergången ske momentant.


Förhämtning är tätt kopplat till mönstret Render-as-you-fetch, beskrivet i React-dokumentationen, där en jämförelse görs mellan andra, traditionella metoder - bl.a. hämtning av data i useEffect - och nyttjande av Suspense.



Sammanfattning

React Suspense väntas bli den etablerade metoden för att styra hämtningen av komponenters resurser inom en snar framtid. Målet med denna artikel har varit att demonstrera - via en exempelapplikation som inkorporerar Suspense tillsammans med ett tredje parts bibliotek för hämtning av data - att verktygen för att bygga mer robusta och bättre presterande webbapplikationer, med överlägsna användarupplevelser, är tillgängliga redan nu.



Marc Klefter | marc@remotifi.com


JavaScript seem to be disabled in your browser.

You must have JavaScript enabled in your browser to utilize the functionality of this website.