Joakim lover å gjøre datalasting i React til en glede

Du trenger ikke Next.js for å bruke Suspense, skriver Bekks Joakim Lindquister.

Joakim Lindquister er utvikler i Bekk. 📸: Bekk / Kurt Lekanger
Joakim Lindquister er utvikler i Bekk. 📸: Bekk / Kurt Lekanger Vis mer

Har du sett React sin Suspense-funksjonalitet og tenkt at det kunne vært noe for applikasjonen din? Bare for å få drømmene lettere lagt i grus når du etter nærmere studier ser at Suspense støttes i hovedsak av metarammeverk som Next.js og Remix?

I denne artikkelen ser vi på ulike måter å håndtere datalasting, og hvordan vi kan forbedre både ytelse og kodekvalitet ved å ta i bruk TanStack Query og Suspense.

Vi omgår metarammeverk-begrensningen og ser på hvordan vi kan bruke Suspense i en tradisjonell klientside React-applikasjon med TanStack Query.

God gammeldags datainnhenting

Før vi dykker ned i TanStack Query og Suspense, la oss raskt se på hvordan man tradisjonelt henter data i en React-applikasjon. Uten en eneste avhengighet bruker vi gjerne en useEffect siden man i React ikke kan gjøre asynkrone operasjoner på toppnivå i en komponent.

export const NisseApp = () => {
	const [gaveønsker, setGaveønsker] = useState<Gaveønske[]>([])
	useEffect(() => {
		fetchGaveønsker().then((data) => setGaveønsker(data))
	})

	if (gaveønsker.length === 0) {
		return <p>Laster..</p>
	}

	return (
		<div>
			<h1>Nissens oversikt over barnas gaveønsker</h1>
			<Gaveønsker gaveønsker={gaveønsker}/>
		</div>
	)
}

Skal man i tillegg ta høyde for at internettlinja mellom nissens kontor på Nordpolen og datasentrene sørafjells er ustabil, bør man også håndtere eventuelle feil.

export const NisseAppMedFeilhåndtering = () => {
    const [gaveønsker, setGaveønsker] = useState<Gaveønske[]>([])
    const [isError, setIsError] = useState(false)
    useEffect(() => {
        fetchGaveønsker()
            .then((data) => setGaveønsker(data))
            .catch(e => setIsError(true))
    })

    if (isError) {
        return <p>Det skjedde en feil 😰 </p>
    }
    ..
}

Denne måten å hente data på er streit nok. Men dette mønstret har noen utfordringer som det kan være verdt å merke seg ⚠️.

Dan Abrahamov, en aktiv bidragsyter til React, har blant annet i denne posten oppsummert noen hovedproblemer:

  • Ingen caching. Hvis en bruker navigerer fram og tilbake mellom en komponent, må hen laste data ved hver eneste render.
  • Race Conditions. Hvis en komponent henter data f.eks basert på en query, er det fort gjort å få bugs med Race Conditions (resultatet avhenger av i hvilken av parallele requests som fullfører sist).
  • Må rendre før man kan begynne å laste. Når en datainnlasting skjer i en useEffect starter ikke datainnlastingen før komponenten rendres (og alle parents er rendret). Dette gjør at det tar unødvendig lang tid, og man får gjerne sekvensiell fossfallslasting av data.

I tillegg blir det en del boilerplatekode av denne måten å fetche data på.

10 år etter React ble lansert, er tendensen at flere og flere frontendapplikasjoner tar i bruk metarammeverk, som Next.js og Remix, som løser mange av de overnevnte problemene for oss. Sitter du imidlertid på en eksisterende "Old School" React-app finnes det heldigvis verktøy i kassa for disse også. 🙏

La oss ta en titt på ett av dem.

TanStack Query: Farvel til useEffect

Et eksempel på et bibliotek som håndterer datalasting og tilstand er TanStack Query. Vi har brukt det med hell på prosjekt og det har et rikt og godt API som gjør hverdagen litt smoothere.

Samme datalasting som i tidligere eksempler kan nå gjøres ved å kalle på useQuery-hooken:

export const NisseApp = () => {
	const { isLoading, isError, data } = useQuery({queryKey: ['cache-key-gaveønsker'], queryFn: fetchGaveønsker})
	if (isLoading || !data) {
		return <p>Laster</p>
	}
	if (isError) {
		return <p>Det skjedde en feil 😭 </p>
	}

	return (
		<div>
			<h1>Nissens oversikt over barnas gaveønsker</h1>
			<Gaveønsker gaveønsker={data}/>
		</div>
	)
}

Ingen useEffect og vi får caching med på kjøpet der altså. useQuery er selve bærebjelken i TanStack Query, i tillegg finnes det også mye annen funksjonalitet for mutations, invalidere cache osv.

Men i eksemplet vårt må komponenten fremdeles sjekke hvordan datalastingen gikk og håndtere innlasting og feilsituasjoner. Hva om man bare kunne kastet disse tilstandene oppover i hierarkiet?

Vel, møt React Suspense. I neste seksjon ser vi på hva det er for noe, før vi avslutter med å se på hvordan vi kan ta det i bruk med TanStack Query.

Vi tar en pause med Suspense

Suspense, lansert i React 16.6, er en innebygd funksjon for å håndtere asynkrone operasjoner, som for eksempel datainnlasting eller fillasting.

Intensjonen bak er å gjøre det enklere å lage gode brukeropplevelser i applikasjoner som laster asynkrone data. Essensen i Suspense lar deler av applikasjonen din ta seg en god gammeldags pause mens en eller flere dataoperasjoner skjer lengre ned i komponenthirarkiet.

Mens deler av applikasjonen tar en pause vises en fallback-komponent som utvikleren selv har definert. Suspense brukes også gjerne sammen med en ErrorBoundary som fanger feil.

export const NisseAppMedSuspense = () => {
	return (
		<div>
			<h1>Nissens oversikt over barnas gaveønsker</h1>
			<ErrorBoundary>
				<Suspense fallback={<p>Laster</p>}>
					<Gaveønsker />
				</Suspense>
			</ErrorBoundary>
		</div>
	)
}

I Gaveønsker-komponenten kan vi nå bruke dataen uten å måtte ta masse forbehold:

const Gaveønsker = () => {
	const { data } = fetchGaveønsker()
	// Data is guaranteed to be defined in this context.
	return <>{data.map(gaveønske => <p>Gaveønske: ${gaveønske}</p>)}</>
}

Dette flytter ansvaret for å håndtere datainnlasting fra selve visningskomponenten som bruker dataen til en komponent lengre oppe i treet, for eksempel en page container. Dette separerer og tydeligjør ansvaret: visningskomponenten kan gjøre det den kan best, vise innhold uten å måtte forholde seg til hvordan det har blitt lastet.

Hvis man laster data fra flere kilder i flere underkomponenter, kan de bruke en felles Suspense og ErrorBoundary slik at alle underkomponenter slipper å håndtere dette selv.

Det er bare et problem med denne koden. Det vil ikke fungere i en vanlig klientside React-applikasjon på grunn av tekniske begrensinger i Suspense 😰

⚠️ Tekniske begrensinger ved Suspense

Leser vi dokumentasjonen til Suspense litt nøyere ser vi at Suspense foreløpig har noen viktige begrensninger:

"Only Suspense-enabled data sources will activate the Suspense component. They include: Data fetching with Suspense-enabled frameworks like Relay and Next.js."

"Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented."

For vår del betyr dette at Suspense ikke vil fungere, hverken for datalasting med useEffect og med TanStack Query sin useQuery. Suspense vil rett og slett aldri oppdage at det er noe data som lastes. Heldigivis så har nylig TanStack Query lansert fullverdig støtte for Suspense som lar oss gjøre nettopp dette. 🙌

💕 Suspense + TanStack Query

Siste versjon av TanStack Query kommer med fullverdig støtte for Suspense og eksponerer tre dedikerte hooks: useSuspenseQuery, useSuspenseInfiniteQuery og useSuspenseQueries. For å bruke Suspense kan vi bare bytte ut kallet vårt til useQuery med useSuspenseQuery.

import { useSuspenseQuery } from '@tanstack/react-query'

const Gaveønsker = () => {
	const { data } = useSuspenseQuery({queryKey: ['cache-key-gaveønsker'], queryFn: fetchGaveønsker})
	// Data is guaranteed to be defined in this context.
	return <>{data.map(gaveønske => <p>Gaveønske: ${gaveønske}</p>)}</>
}

I denne konteksten er data garantert til å være definert. useSuspenseQuery returnerer ikke loading eller isError flagg, men "kaster" feilen så dette blir håndtert av en ErrorBoundary eller en Suspense definert høyere opp i komponenthirarkiet.

Dette gjøres på samme måte som vi så på i et tidligere eksempel:

export const NisseAppMedSuspense = () => {
	return (
		<div>
			<h1>Nissens oversikt over barnas gaveønsker</h1>
			<ErrorBoundary>
				<Suspense fallback={<p>Laster</p>}>
					<Gaveønsker />
				</Suspense>
			</ErrorBoundary>
		</div>
	)
}

Til slutt

Vi har i denne artikkelen dukket nærmere inn på datalastning i React. Vi har sett på problemene med å hente data med useEffect, vi har i stedet sett på TanStack Query som tar seg av mye av den harde jobben.

Videre har vi sett på hvordan vi kan bruke Suspense og ErrorBoundaries for å slippe å håndtere datainnlasting med all kompleksitet i visningskompontene.

God koding!