Hopp til hovedinnhold

Det føles ofte ut som at hverdagen min som frontend-utvikler består av to ting. Lage skjemaer og håndtere data-fetching. Det er der det meste av tiden går bort i alle fall. Og data-fetching har alltid vært ganske kjipt. Men, med React Suspense, er dette blitt en hel del kjekkere!

En illustrasjon av et "loading skeleton", en midlertidig visning som skal se ut som innholdet som lastes inn.

Ikke alt var bedre før...

function Julegaver() {
  const [gaver, setGaver] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function hentGaver() {
      setIsLoading(true);
      setGaver(await hentGaver());
      setIsLoading(false);
    }
    hentGaver();
  }, []);
  
  if(isLoading) {
    return (
      <p>Julenissen har ikke kommet med gavene enda...</p>
    );
  }

  return (
    <ul>
      {gaver.map((gave) => <li key={gave.id}>{gave.innhold}</li>)}
    </ul>
  );
}

Et sabla rot med flere forskjellige states og useEffect som du må sjonglere med. Dette var faktisk sånn vi gjorde data-fetching i gamle dager. Og dette er egentlig en ganske forenklet versjon... Heldigvis kom julenissen, eller Next.js-teamet og Tanner Linsley var det vel egentlig, etter hvert med gaver til React-miljøet som skulle gjøre alt dette enklere, nemlig swr og react-query. Med disse pakkene ble livene våre en hel del bedre. Data-fetching krevde ikke lenger den samme sjongleringen med states og useEffects lengre. Alt ble håndtert for deg:

import { useQuery } from "@tanstack/react-query";

function Julegaver() {
  const { data: gaver, isLoading } = useQuery({
    queryKey: ["gaver"],
    queryFn: hentGaver
  });

  if(isLoading) {
    return (
      <p>Julenissen har ikke kommet med gavene enda...</p>
    );
  }

  return (
    <ul>
      {gaver?.map((gave) => <li key={gave.id}>{gave.innhold}</li>)}
    </ul>
  );
}

Nå var det eneste du trengte å forholde deg til visning; er dataen kommet og vis frem dataen.

Men, mye vil ha mer... Ååååå så kjipt det er at vi fortsatt må kludre til komponent-logikken vår med å sjekke om dataen faktisk er kommet frem i komponenten eller ikke! Hvorfor skal livet mitt være så vanskelig!!!

Suspense til unnsetning

Suspense har faktisk vært en del av React i litt over syv år nå! Den kom ut i React 16.6, 23. oktober 2018, som en del av code-splitting/lazy loading av komponenter funksjonaliteten til React. Dette var faktisk så tidlig i Reacts historie at hooks ikke en gang hadde kommet ut! Suspense sin rolle i "code-splitting pakken" var å kunne tilby funksjonalitet for å vise frem midlertidig innhold før en komponent hadde blitt lastet inn.

import { lazy, Suspense } from 'react';
const Alver = lazy(() => import('./Alver'));

function Nordpolen() {
  return (
    <Suspense fallback={<p>Alvene har ikke våknet enda...</p>}>
      <Alver />
    </Suspense>
  );
}

Og på dette tidspunktet var det alt Suspense gjorde. Men, React-teamet hadde allerede da større planer for Suspense. De så at dette mønsteret kunne gjenbrukes for andre former for innlasting, og planla å innføre Suspense for data-fetching et drøyt halvt år senere, en gang i midten av 2019, som en minor-versjon til React 16.

Men, sånn ble det ikke helt... For det var først 29. mars 2022, i forbindelse med lanseringen av React v18 at Suspense kunne bli brukt for data-fetching. Endelig kunne vi hente data, og la Suspense håndtere innlastingen for oss!

Hvordan bruke Suspense?

Den enkleste måten å ta i bruk Suspense for data-fetching er å ta i bruk et tredjepartsbibliotek som har støtte for data-fetching med suspense. De tidligere nevnte pakkene, SWR og react-query har begge støtte for dette. For eksempel kan du i react-query bare bytte ut useQuery med useSuspenseQuery så er du i gang:

import { useSuspenseQuery } from "@tanstack/react-query";

function Julegaver() {
  const { data: gaver, isLoading } = useSuspenseQuery({
    queryKey: ["gaver"],
    queryFn: hentGaver
  });

  return (
    <ul>
      {gaver.map((gave) => <li key={gave.id}>{gave.innhold}</li>)}
    </ul>
  );
}

function App() {
  return (
    <Suspense fallback={
      <p>Julenissen har ikke kommet med gavene enda...</p>
    }>
      <Julegaver />
    </Suspense>
  );
}

Med dette oppsettet slipper vi i <Julegaver>-komponenten vår å tenke på datalasting i det hele tatt. Alt blir tatt hånd om gjennom Suspense! Dette gjør at komponenten bare trenger å forholde seg til "happy path" i implementasjonen, noe som gjør det betraktelig mye enklere å lese og vedlikeholde.

Men, nå som React 19 er ute kan vi også bruke Suspense for data-fetching helt uten tredje-parts biblioteker. I React 19 fikk vi nemlig en ny hook, use, som kan brukes for å vente på Promiser gjennom Suspense.

import { use } from "react";

function Julegaver({ gavePromise }) {
  const gaver = use(gavePromise);
  return (
    <ul>
      {gaver.map((gave) => <li key={gave.id}>{gave.innhold}</li>)}
    </ul>
  );
}

function App() {
  const gavePromise = hentGaver();
  return (
    <Suspense fallback={
      <p>Julenissen har ikke kommet med gavene enda...</p>
    }>
      <Julegaver gavePromise={gavePromise} />
    </Suspense>
  );
}

Hvordan fungerer dette?

Når use tar imot et Promise som argument så suspender den rendringen av komponenten. Altså, stopper rendring av komponenten opp, og det blir sendt en melding til nærmeste Suspense-boundary om at den må vente på at Promise-et er resolved før vi kan fortsette rendring. Suspense-boundaryen følger da med på dette Promise-et, og når julenissen har vært innom med gavene, og resolvet Promise-et, så vil vi da prøve å rendre komponentene inne i Suspense-kroppen på nytt. Neste gang den da møter på use(gavePromise) vil vi se at Promise-et er resolved, og at vi bare kan ta i bruk resultatet. Vi får dermed en garanti på at når vi er kommet oss forbi dette use-kallet, at julenissen har vært innom!

Men, du la kanskje merke til at vi flyttet data-fetchingen ut av selve komponenten. Det var ikke helt tilfeldig... For som nevnt vil jo use suspende rendring av komponenten hvis den har en Promise som ikke er resolved i seg. Og når vi suspender en komponent før den har mountet for første gang vil vi ikke beholde noe av tilstanden til komponenten. Så hadde vi flyttet den faktiske data-fetchingen inn i komponenten, som i eksempelet med react-query, så hadde vi bare lagd et nytt Promise hver gang vi prøvde å rendre komponenten på nytt. Vi hadde altså endt opp i en evig suspense-loop.

import { use } from "react";

function Julegaver() {
  // Kommer oss aldri forbi denne, da promiset blir lagd på nytt hver gang.
  // Vi sitter fast i en uendlig suspense-loop...
  const gaver = use(hentGaver()); 
  return (
    <ul>
      {gaver.map((gave) => <li key={gave.id}>{gave.innhold}</li>)}
    </ul>
  );
}

For å unngå dette kan vi gjøre som vi gjorde i eksempelet over, flyttet det asynkrone kallet ut av Suspense-boundary, eller på en eller annen måte cache'e oppretting av promiset. Det er faktisk det react-query gjør! react-query har funksjonalitet for å deduplisere kall ved bruk av useQuery ved å cache disse queryene basert på den gitte queryKey-en. Dette spiller heldigvis ganske godt på lag med Suspense, da react-query gjennom denne cachingen får hentet opp igjen det opprinnelige promiset når komponenten forsøkes å re-rendre etter en suspense er ferdig å vente. Da unngår vi helt problematikken rundt uendelig suspense-loops!

Suspense og julefred

Nå er det altså fullt mulig å lage komponenter som suspender, ved hjelp av den nye use-hooken til React. Men, som vi har sett er det fort gjort å bli fanget i suspense-loop fella om du ikke passer deg for den. Så hvis alle sammen snart skal få feire jul igjen, så kan det nok fortsatt være lurt å gå for et suspense-kompatibelt bibliotek, som SWR og react-query.

Sånn sørger du for at du trygt kan gå inn i juleferien med lave skuldre.

Liker du innlegget?

Del gjerne med kollegaer og venner