Hopp til hovedinnhold

Hørt om Suspense og Error Boundaries, men synes det er litt abstrakt og vanskelig å bruke? La oss lage en komponent som løser det vanskelige for deg.

En vulkan som har utbrudd

React har utviklet seg en god del i det siste. Det kommer stadig flere features, og det er mer og mer å lære seg – men det betyr ikke at det trenger å være så vanskelig å lage gode brukergrensesnitt.

I dag skal vi bygge en komponent du kan wrappe kode som vises data som kanskje ikke er klar enda.

Steg 1: Lag en asynkron komponent

Asynkrone komponenter er en komponent som suspender – det vil si en komponent som kaller den nye use-hooken til React. Den tar (blant annet) i mot en promise, og "kaster promiset" om det ikke er klart enda. Om promiset feiler (eller rejecter, som det heter), så kaster use-hooken en feil.

Slik kan det se ut:

type Props = { userPromise: Promise<User> }

function UserSalute({ userPromise }: Props) {
  const user = use(userPromise);
  return <h1>Hei {user.name}</h1>
}

Steg 2: Wrap den i en Suspense-boundary

Om dette er hele appen din, kræsjer det ganske fort. Du trenger nemlig å fange disse "kastede" promisene i en såkalt suspense boundary. Der sender du inn den asynkrone komponenten din som children, og en fallback-komponent for når man venter. Slik kan det se ut i en React Router app:

export default function App() {
  const { userPromise } = useLoaderData();
  return (
    <Suspense fallback={<Skeleton />}>
      <UserSalute user={userPromise} />
    </Suspense>
  )
}

export const loader = ({ request }: LoaderFunctionArgs) => {
  return {
    userPromise: getUser(request)
  };
};

Steg 3: Wrap den i en ErrorBoundary

Ulempen med asynkron data, er at den kan feile. Serveren kan kaste en feil, responsen kan være feil, eller internett kan falle ut halvveis. Og når det skjer, er det viktig å gi en feilmelding til brukeren som de kan gjøre noe med.

Error boundaries i React er fortsatt den eneste tingen man trenger klassekomponenter for å implementere. Husker du klasse-komponenter? Det gjør knapt jeg, og jeg jobba med dem i sikkert 5 år. Heldigvis har det kommet en bitteliten offisielt anbefalt pakke – react-error-boundary – som gir oss en gjenbrukbar komponent med et enkelt og greit API å bruke dem på.

La oss legge den til:

import { ErrorBoundary } from "react-error-boundary";

export default function App() {
  const { userPromise } = useLoaderData();
  return (
    <ErrorBoundary fallback={<h1>Hei på deg!</h1>}>
      <Suspense fallback={<Skeleton />}>
        <UserSalute user={userPromise} />
      </Suspense>
    </ErrorBoundary>
  )
}

export const loader = ({ request }: LoaderFunctionArgs) => {
  return {
    userPromise: getUser(request)
  };
};

Om du ønsker å tracke at feilen oppstod til en eller annen error reporting-tjeneste, kan du få tilgang til hva som gikk feil ved å bruke fallbackRender:

import { ErrorBoundary } from "react-error-boundary";
import { reportError } from "~/services/error-reporting"

export default function App() {
  const { userPromise } = useLoaderData();
  return (
    <ErrorBoundary 
      fallback={<h1>Hei på deg!</h1>}
      onError={(error, stackTrace) => {
        reportError(error, stackTrace);
      }}
    >
      <Suspense fallback={<Skeleton />}>
        <UserSalute user={userPromise} />
      </Suspense>
    </ErrorBoundary>
  )
}

export const loader = ({ request }: LoaderFunctionArgs) => {
  return {
    userPromise: getUser(request)
  };
};

En ting som kan være greit å huske på er at error boundaries kun fungerer på klientsiden. Så om du driver med server-rendering (som i dette eksempelet, egentlig), vil de ikke funke. Da må du heller bruke verktøy du får fra rammeverket (en ErrorBoundary eksport i vårt eksempel, eller en error.tsx fil i Next.js). Grunnen til at de allikevel fungerer over, er fordi vi resolver et promise, og det skjer på klientsiden!

La oss gjøre det gjenbrukbart!

Det vi har gjort over, er noe vi kan bruke flere steder. Så la oss refaktorere litt for å gjøre den gjenbrukbar:

import { ErrorBoundary } from "react-error-boundary";
import { reportError } from "~/services/error-reporting"

type ResolvingComponentProps = {
  errorFallback: React.ReactNode;
  loadingFallback: React.ReactNode;
  children: React.ReactNode;
}
export function ResolvingComponent({ 
  errorFallback, 
  loadingFallback, 
  children 
}) {
  return (
    <ErrorBoundary 
      fallback={errorFallback} 
      onError={(error, info) => { 
        reportError(error, info);
      }}
    >
      <Suspense fallback={loadingFallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  )
}

export default function App() {
  const { userPromise } = useLoaderData();
  return (
    <ResolvingComponent 
      errorFallback={<h1>Hei på deg!</h1>}
      loadingFallback={<Skeleton />}
    >
      <UserSalute user={userPromise} />
    </ResolvingComponent>
  );
}

export const loader = ({ request }: LoaderFunctionArgs) => {
  return {
    userPromise: getUser(request)
  };
};

Avhengig av hvordan din applikasjon ser ut, kan du også legge til default fallbacks for feilmeldinger eller loading states.

Vi bruker dette mønsteret overalt i koden vår, da vi har mange asynkrone kall som kan feil av forskjellige grunner. Kanskje det kan være noe for deg også?

Liker du innlegget?

Del gjerne med kollegaer og venner