4. Fetching data
4. Fetching data
Tijdens deze les bekijken we hoe we data kunnen ophalen via HTTP request in een React applicatie en hoe we deze kunnen verwerken bij een succesvol request, maar ook hoe we mislukte requests gracieus kunnen afhandelen.
We illustreren deze concepten aan de hand van een applicatie waarmee het weerbericht voor een willekeurige locatie bekeken kan worden.
Info
Als je de voorbeeldcode wil uitvoeren moet je zelf een API key aanvragen bij Open Weather Map.
Startbestanden
Nieuwe hooks schrijven
Zelf een hook schrijven is niet bijzonder moeilijk, een hook is tenslotte niets anders dan een functie.
Begrip: Custom Hook
Een custom hook is een hook die je zelf schrijft met als doel herbruikbare code af te zonderen in een aparte functie die later herbruikt kan worden in verschillende componenten. De conventie is dat de naam van deze functie begint met use. Binnen een hook kunnen we alle gekende hooks (en custom hooks) gebruiken, hiervoor gelden dezelfde regels als in een component. Een custom hook kan eender wat teruggeven, net zoals een normale JavaScript functie. Daarnaast kunnen er ook nul, één of meer parameters aanwezig zijn in de definitie van de hook.
const useSomeReusableCode = (...) => {
// Do something reusable
return ...
}useDebouncedState
De startbestanden bevatten al een formulier dat we kunnen gebruiken om de locatie in te geven waarvoor we het weerbericht willen zien. Het weerbericht wordt natuurlijk niet lokaal bewaard, maar wordt via een HTTP request gedownload van een API.
Een gebruiker moet de mogelijkheid hebben om de locatie te wijzigen, maar als developer willen we niet dat er bij elke wijziging een verzoek naar de server gestuurd wordt. Ten eerste heeft een verzoek geen zin als de gebruiker enkel 'G' heeft ingegeven in de plaats van de volledige zoekterm 'Geel'. Ten tweede willen we het aantal requests naar de API-server beperken omdat elk verzoek geld kost en de app wordt daarbovenop trager omdat we veel onnodige request versturen. We zijn enkel geïnteresseerd in het laatste verzoek, maar alle voorgaande requests moeten wel afgehandeld of geannuleerd worden.
Om deze problemen te vermijden schrijven we wrapper rond de useState hook die als derde element de state teruggeeft, maar pas na een bepaalde tijd. Als de gebruiker stopt met typen (bijvoorbeeld 500ms nadat de laatste toets ingedrukt is), wordt het derde element aangepast.
Om de hook zo herbruikbaar mogelijk te maken, gebruiken we een generische parameter. De hook krijgt verder twee normale parameters, defaultValue en debounceTime. De eerste parameter, defaultValue, bepaald de standaard waarde die we aan de interne useState calls meegeven. De tweede optionele parameter, debounceTime, bepaald hoe lang we wachten voordat we het derde element in de array aanpassen.
We komen zo tot een eerste versie van de useDebouncedState hook die weliswaar nog niet volledig werkt. Merk op dat we als return type een triple[1] gebruiken waarvan het tweede element het type Dispatch<SetStateAction<T>> heeft. Dit is hetzelfde type als het tweede element in het paar[2] dat door de useState hook teruggegeven wordt.
import {Dispatch, SetStateAction, useState} from 'react'
type UseDebouncedStateResult<T> = [T, Dispatch<SetStateAction<T>>, T]
const useDebouncedState = <T>(defaultValue: T, debounceTime: number = 500): UseDebouncedStateResult<T> => {
const [value, setValue] = useState<T>(defaultValue)
const [debouncedValue, setDebouncedValue] = useState<T>(defaultValue)
return [value, setValue, debouncedValue]
}De hook doet natuurlijk nog niet bijzonder veel, de debounceTime parameter wordt nog niet gebruikt en de debouncedValue wordt nooit aangepast.
We breiden de hook verder uit door de implementatie van het tweede element in de return array aan te passen. We voorzien een nieuwe functie die een timeout start waarna de debouncedValue aangepast wordt. Het is natuurlijk ook nodig om eventuele timeouts die nog lopen te annuleren als de functie herhaaldelijk opgeroepen wordt voordat de timeout verstreken is. Anders zou de debouncedValue te veel aangepast worden.
Merk op dat we opnieuw gebruik maken van het Dispatch<SetStateAction<T>> type. De parameter van de functie updateValue en timeoutFinishedHandler is van SetStateAction<T>, met andere woorden iets dat je aan de setter van een useState hook kan meegeven.
Deze nieuwe hook kan vervolgens gebruikt worden in de Home component en gekoppeld worden aan het input veld. Voorlopig negeren we het derde element in de return array.
import {Dispatch, SetStateAction, useState} from 'react'
type UseDebouncedStateResult<T> = [T, Dispatch<SetStateAction<T>>, T]
const useDebouncedState = <T>(defaultValue: T, debounceTime: number = 500): UseDebouncedStateResult<T> => {
const [value, setValue] = useState<T>(defaultValue)
const [debouncedValue, setDebouncedValue] = useState<T>(defaultValue)
let timeoutId: number | null = null
const timeoutFinishedHandler = (setStateAction: SetStateAction<T>) => {
timeoutId = null
setDebouncedValue(setStateAction)
}
const updateValue: Dispatch<SetStateAction<T>> = setStateAction => {
// Als er al een timeout ingesteld was,
// moet deze geannuleerd worden.
if (timeoutId) {
clearTimeout(timeoutId)
}
setValue(setStateAction)
// Na de debounce time kan de nieuwe waarde ingesteld worden.
timeoutId = setTimeout(() => timeoutFinishedHandler(setStateAction), debounceTime)
}
return [value, updateValue, debouncedValue]
}const Home: FunctionComponent = () => {
const [city, setCity] = useDebouncedState<string>('Geel')
return (
<>
<h2 className="text-5xl">Weerbericht</h2>
<div className="my-4">
<Label htmlFor="city">Email</Label>
<Input
type="text"
id="city"
value={city}
onChange={e => setCity(e.target.value)}
placeholder="Geel"
/>
</div>
</>
)
}useRef als persistente variabele
Bovenstaande code lijkt op het eerste zicht misschien te werken, maar als we de Home component uitbreiden en de waarde van de debouncedValue tonen, zien we dat er nog een probleem is. De debouncedValue wordt constant aangepast terwijl dit pas debounceTime milliseconden na de laatste wijziging zou mogen gebeuren.
const Home: FunctionComponent = () => {
const [city, setCity, debouncedCity] = useDebouncedState<string>('Geel')
return (
<>
<h2 className="text-5xl">Weerbericht</h2>
<div className="my-4">
<Label htmlFor="city">Email</Label>
<Input
type="text"
id="city"
value={city}
onChange={e => setCity(e.target.value)}
placeholder="Geel"
/>
</div>
<p>Debounced city: {debouncedCity}</p>
</>
)
}De reden voor het probleem is de timeoutId variabele in de hook, dit is een lokale variabele die enkel bestaat zolang de functie uitgevoerd wordt. De variabele wordt aangemaakt als de functie start en wordt weer verwijderd als de functie afgehandeld is. Deze variabele in de state plaatsen is geen goede keuze omdat de variabele geen rechtstreekse invloed heeft op het UI en dus tegen het doel van state ingaat. Een wijziging in de state moet altijd een rerender tot gevolg hebben. De useRef hook biedt een oplossing.
Begrip: useRef als persistente variabele
Via de useRef hook kan je een variabele persistent maken doorheen renders, i.e. tussen verschillende keren dat een functiecomponent opgeroepen wordt of de state aangepast wordt.
Net zoals de useState hook heeft de useRef hook een generische parameter waarmee het type van de inhoud van de inhoud van de hook bewaard kan worden.
De waarde van de _useRef* hook zit steeds in de current property. Dus de signatuur van de hook ziet er (ongeveer) als volgt uit.
import {FunctionComponent, useRef} from 'react'
const SomeComponent: FunctionComponent = () => {
const persistentVariable = useRef<string>(defaultValue)
const foo = () => {
if (persistentVariable.current === bar) {
// Do something
// Update the persistent variable
persistentVariable.current = 'baz'
}
}
return <p onClick={foo}>Some JSX code</p>
}Via de useRef hook kunnen we dus het timeoutId bijhouden zodat dit tussen verschillende calls van de useDebouncedState hook zijn waarde niet verliest.
const useDebouncedState = <T>(defaultValue: T, debounceTime: number = 500): UseDebouncedStateResult<T> => {
const [value, setValue] = useState<T>(defaultValue)
const [debouncedValue, setDebouncedValue] = useState<T>(defaultValue)
const timeoutId = useRef<number | null>(null)
const timeoutFinishedHandler = (setStateAction: SetStateAction<T>) => {
timeoutId.current = null
setDebouncedValue(setStateAction)
}
const updateValue: Dispatch<SetStateAction<T>> = setStateAction => {
if (timeoutId.current) {
clearTimeout(timeoutId.current)
}
setValue(setStateAction)
timeoutId.current = window.setTimeout(() => timeoutFinishedHandler(setStateAction), debounceTime)
}
return [value, updateValue, debouncedValue]
}Zoals in onderstaande video te zien is, wordt de debouncedValue nu pas aangepast nadat de gebruiker gestopt is met typen.
Environment variables
Om een overzicht te houden over de verschillende API keys die we gebruiken, is het handig om deze op één centrale plek te bewaren. We gebruiken hiervoor het .env bestand dat in de root van het project geplaatst wordt.
Begrip: Environment Variables
Voor projecten aangemaakt via vite kan een .env file gebruikt worden. In deze file worden environment variabelen opgeslagen die beschikbaar moeten zijn in de frontend code, let op, hiervoor moet de prefix VITE_ toegevoegd worden aan de naam van de variabele. De .env file moet in de root van je project bewaard worden.
Via het import.meta.env object kunnen de verschillende environment variabelen opgevraagd worden.
Gevoelige API Keys
Alhoewel wij dit bestand gebruiken om een API key te bewaren is dit niet veilig. Enkel API keys die bedoeld zijn om publiek beschikbaar te zijn (i.e. in een website), kunnen hier bewaard worden. De .env file wordt mee in de production build van de applicatie gezet waardoor elke key gelezen kan worden door al je gebruikers.
Het is meestal beter om je API keys op de server te gebruiken zodat je authenticatie en autorisatie kan toevoegen en kan garanderen dat je keys niet misbruikt worden.
VITE_ENVIRONMENT_VARIABLE=some-string-value-hereconst ENVIRONMENT_VARIABLE = import.meta.env.VITE_ENVIRONMENT_VARIABLE:::
In het voorbeeld voegen we dus onderstaande key toe. De as string cast is noodzakelijk omdat een key eventueel niet aanwezig kan zijn, in dat geval is de waarde undefined. We gaan er echter vanuit dat de key altijd aanwezig is, als we de .env file zouden vergeten zal de applicatie gewoon crashen.
VITE_OPEN_WEATHER_API_KEY=jouw-api-key-hierconst apiKey = import.meta.env.VITE_OPEN_WEATHER_API_KEY as stringAPI Aanspreken
In de startbestanden zijn reeds twee functies voorzien waarmee we twee endpoints van de Open Weather API aanspreken. Aangezien deze functies fetch gebruiken en dit kennis is die je uit het opo JavaScript zou moeten hebben, gaan we hier niet verder op in.
De getCoordinates methode haalt de coördinaten van een locatie op via het geocoding endpoint. De getWeather methode haalt het weerbericht op voor een bepaalde locatie via het 5 day weather forecast endpoint.
const getCoordinates = async (query: string): Promise<Coordinates> => {
if (query === '') {
throw new Error('Invalid location')
}
// Wacht 500 milliseconden voordat de query gestart wordt om een traag netwerk te simuleren en
// Suspense te demonstreren.
await new Promise<void>(resolve => setTimeout(() => resolve(), 500))
const url = new URL('/geo/1.0/direct', baseUrl)
url.searchParams.set('q', query)
url.searchParams.set('appid', apiKey)
const response = await fetch(url)
const data = (await response.json()) as IGeoCodeResult[]
return [data[0].lat, data[0].lon]
}
const getWeather = async ([latitude, longitude]: Coordinates): Promise<IFiveDayForecast> => {
await new Promise<void>(resolve => setTimeout(() => resolve(), 500))
const url = new URL('/data/2.5/forecast', baseUrl)
url.searchParams.set('lat', latitude.toString())
url.searchParams.set('lon', longitude.toString())
url.searchParams.set('appid', apiKey)
url.searchParams.set('units', 'metric')
url.searchParams.set('lang', 'nl-be')
const response = await fetch(url)
return (await response.json()) as IFiveDayForecast
}TanStack Query
TanStack Query vroeger gekend als React Query, is een library die het heel eenvoudig maakt om op een goede manier om te gaan met server data.
De library houdt een cache bij van alle requests, ook als de component die de query verstuurd heeft niet langer mounted (zichtbaar) is in de UI. Omwille van deze cache vervangt TanStack query, in de meeste gevallen, global-state libraries zoals Redux, MobX, Recoil, Jotai, ... dit is dan ook de reden dat geen van deze libraries besproken wordt in deze cursus.
Naast cachen van data, is het via TanStack Query ook eenvoudig om data elke minuten of seconden te verversen, om aan te geven hoelang de cache geldig is, om een fetch request automatisch terug uit te voeren als het tabblad met de website geselecteerd wordt en nog veel meer.
Het is mogelijk om in React netwerk requests te schrijven zonder gebruik te maken van TanStack Query, of een soortgelijke tool. De meeste tutorials die je hierover online vindt, maken gebruik van de useEffect hook. Since het voorjaar van 2022 is hier echter veel kritiek op gekomen vanuit het React team, onderstaande video en de React documentatie gaan dieper in op de problemen met het gebruik van useEffect voor data fetching. De mening van het React team is momenteel dat je ofwel een framework zoals Next.js of Remix moet gebruiken of een fetching library zoals TanStack Query of SWR.
Vanaf React 19 kan ook de use functie gebruikt worden om data op te halen in eenvoudige applicaties.
Stale while revalidate
TanStack Query werkt volgens het stale-while-revalidate principe.
Begrip: Stale While Revalidate
Het stale while revalidate (SWR) principe betekent dat zodra het antwoord op een request binnen komt, er een timer begint te lopen die aangeeft hoelang de data nog als juist/correct/recent beschouwd mag worden. De default waarde (in TanStack Query) is 0 seconden, data wordt dus als stale (verouderd) beschouwd vanaf dat deze opgehaald is.
Zodra een gebruiker terug focust op de pagina, of op een andere manier naar nieuwe data vraagt, wordt de data uit de cache gebruikt om de pagina zo snel mogelijk te tonen, maar er wordt ook onmiddellijk een nieuw verzoek gestuurd om de data te refreshen. Standaard blijft data (in TanStack Query) 5 minuten in de cache zitten.
TanStack Query configureren
Om TanStack Query te configureren moeten de componenten die gebruik maken van de library omringd worden door een provider die een queryClient instantie aanbiedt. Deze provider maakt intern gebruik van context. We kunnen dus meerdere providers gebruiken in één applicatie als dit nodig blijkt, bijvoorbeeld omdat de data op bepaalde onderdelen veel sneller ververst moet worden dan op andere onderdelen van de applicatie. Dit resultaat kan echter ook bekomen worden door de configuratie van de queries aan te passen.
We gebruiken grotendeels de basisconfiguratie en passen slechts 1 parameter aan. De refetchOnWindowFocus optie bepaald of data opnieuw opgehaald moet worden als het tabblad met de website geselecteerd wordt. In productie is dit een goed idee omdat je altijd de laatste data wilt gebruiken, voor development kan dit echter problemen veroorzaken. Een focus event wordt ook afgevuurd als je van de dev tools naar de browser gaat of wanneer je wisselt tussen je IDE en de browser. Dit kan heel wat requests veroorzaken die niet nodig zijn en de kost van je API requests snel doen oplopen. We zetten deze optie dus uit in development. De andere mogelijke parameters zijn bijna exact dezelfde als diegene voor de useQuery hook die verder besproken wordt.
import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: import.meta.env.PROD,
},
},
})
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<div className="m-4">
<Home />
</div>
</QueryClientProvider>
</React.StrictMode>,
)Suspense
Begrip: Suspense
De Suspense component kan gebruikt worden om een fallback element, zoals een spinner of loading animatie, te tonen terwijl data aan het laden is. Zodra de data geladen is, wordt het fallback verborgen en wordt de data getoond.
Deze component is enkel beschikbaar als je een library gebruikt die hier ondersteuning voor biedt of als je de use operator uit React 19 gebruikt.
import {FunctionComponent, Suspense} from 'react'
const SomeComponent: FunctionComponent = () => {
return (
<Suspense fallback={<SomeFallbackComponent/>}>
<SomeComponentThatLoadsDataThroughALibraryThatSupportsSuspense/>
</Suspense>
)
}In de startbestanden is een Loading component beschikbaar die een spinner toont. We gebruiken suspense om deze spinner te tonen terwijl de Weather component data aan het laden is.
Merk op dat Weather meegegeven wordt als kind aan de Suspense component. Dit is noodzakelijk, de data moet opgehaald worden in een kind, anders werkt de suspense niet.
const Home: FunctionComponent = () => {
const [city, setCity, debouncedCity] = useDebouncedState<string>('Geel')
return (
<>
<h2 className="text-5xl">Weerbericht</h2>
<div className="my-4">
<Label htmlFor="city">Email</Label>
<Input
type="text"
id="city"
value={city}
onChange={e => setCity(e.target.value)}
placeholder="Geel"
/>
</div>
<Suspense fallback={<Loading />}>
<Weather city={debouncedCity} />
</Suspense>
</>
)
}Momenteel doet bovenstaande code nog niets, daarvoor moeten we eerste data ophalen in de Weather component.
useSuspenseQuery
Begrip: useSuspenseQuery
De useSuspenseQuery hook wordt gebruikt om data op te halen van een externe bron. Wat deze bron is, speelt geen rol. Dit kan, afhankelijk van het platform (web, desktop, mobile), een API, een filesystem operatie, een sqlite query of nog iets anders zijn.
De hook is zeer uitgebreid en complex, we beperken ons in deze tekst tot de basisfunctionaliteiten. Voor meer details verwijzen we door naar de documentatie.
Deze useSuspenseQuery hook heeft 2 vaste parameters en één optionele:
Een array waarvan de elementen samen de key van de query vormen. Deze key moet uniek zijn, als 2 queries dezelfde key krijgen, worden ze als gelijk beschouwd en wordt eventueel gecachete data gebruikt.
Een functie die data ophaalt.
Optionele configuratie, de lijst van mogelijke opties is te groot om hier te bespreken, we verwijzen door naar de documentatie
De useSuspenseQuery hook geeft een groot object terug, we verwijzen opnieuw naar de documentatie voor de volledige lijst. We gebruiken in deze les enkel de data property.
import {FunctionComponent} from 'react'
import {useSuspenseQuery} from '@tanstack/react-query'
const SomeComponent: FunctionComponent = () => {
const {data} = useSuspenseQuery({
queryKey: ['some', 'unique', 'query', 'key'],
queryFn: someAsynchronousFetchingFunction,
// Optional extra configuration
})
return <>{/* Some JSX that uses the fetched data. */}</>
}Hoewel de useSuspenseQuery[3] gebruikt kan worden in een normale component, zullen we deze calls steeds afzonderen in een nieuwe hook. Zo kunnen we eenzelfde query gebruiken in meerdere pagina's of componenten en blijven onze componenten compacter en beter leesbaar. Dit kan bijvoorbeeld handing zijn als je verschillende views voor dezelfde data wilt bouwen.
We schrijven dus een nieuwe hook, useGetCoordinates, die de coördinaten van een locatie ophaalt op basis van een gegeven query. Merk op dat we in het return-type van de hook de generische UseSuspenseQueryResult interface gebruiken, de eerste parameter is soort data dat opgehaald wordt door de query, het tweede element is het soort error dat kan optreden. Aangezien we geen custom error objecten gebruiken, kiezen we hier voor de standaard Error interface.
Naast de coördinaten moeten we ook het weerbericht ophalen, hiervoor schrijven we wederom een nieuwe hook. De structuur van deze nieuwe hook is vrijwel identiek aan die van de vorige hook, we geven andere waarden mee aan de parameters, maar over het algemeen is er veel gelijkenis tussen de twee nieuwe hooks.
import {useSuspenseQuery, UseSuspenseQueryResult} from '@tanstack/react-query'
export const useGetCoordinates = (query: string): UseSuspenseQueryResult<Coordinates, Error> => {
return useSuspenseQuery({
queryKey: ['geocode', query],
queryFn: () => getCoordinates(query),
})
}
export const useGetWeather = (coordinates: Coordinates): UseSuspenseQueryResult<IFiveDayForecast, Error> => {
return useSuspenseQuery({
queryKey: ['weather', coordinates],
queryFn: () => getWeather(coordinates),
})
}We kunnen deze hooks vervolgens gebruiken in de Weather component. Merk op dat we de data properties hernoemen naar coordinates en weather om duidelijk te maken wat voor data er in die variabelen zit.
Omdat we suspense gebruiken worden de queries sequentieel uitgevoerd, eerst worden de coördinaten opgehaald en pas als deze geladen zijn het weerbericht. Dit is eigen aan hoe suspense werkt.
const Weather: FunctionComponent<CoordinatesProps> = ({city}) => {
const {data: coordinates} = useGetCoordinates(city)
const {data: fiveDayForecast} = useGetWeather(coordinates)
const groupedByDay: Record<string, IOneDayForecast[]> = {}
fiveDayForecast.list?.forEach(item => { ... })
return (
<div>
{Object.keys(groupedByDay).map(day => ( ... )}
</div>
)
}Bovenstaande code resulteert in het volgende resultaat, merk op dat we voor elke nieuwe stad de Loading component te zien krijgen. Als we echter terugkeren naar een vorige stad zien we ogenblikkelijk het resultaat. Dit komt doordat de data van de vorige steden bewaard is in de cache en dus niet gefetch moet worden.
Verouderde data & caching
Alhoewel we, in bovenstaande video, onmiddellijk een resultaat zien voor reeds bezochte steden zien we in het netwerk tab toch dat er nog twee requests verstuurd worden naar de Open Weather API. Dit komt doordat we met het stale-while-revalidate principe werken. De data wordt als verouderd beschouwd van het moment dat deze gedownload is, als we die data terug opvragen wordt de cache gebruikt, maar wordt de data ook opnieuw opgehaald.
Voor het weerbericht is dit geen probleem, deze informatie is namelijk heel tijdgevoelig en kan snel wijzigen. Voor de coördinaten is dit echter niet het geval, geografische coördinaten liggen vast en wijzigen nooit. Het heeft dus geen zin om nieuwe coördinaten op te halen, dit is een extra API request dat eenvoudig vermeden kan worden.
Via de stateTime parameter geven we aan dat de coördinaten nooit verouderd zijn, hiervoor gebruiken we de constante Infinity. Via de gcTime parameter geven we aan hoe lang de data in de cache bewaard moet blijven, we zouden hier wederom voor Infinity kunnen kiezen, maar dan blijft deze data in het RAM staan. Aangezien het om heel weinig data gaat, is dit waarschijnlijk geen probleem, maar je weet nooit of iemand je website gebruikt met een heel oude budget smartphone die over weinig RAM beschikt. We verhogen de tijd dat deze data bewaard blijft, maar zetten deze niet op Infinity en kiezen voor 10 minuten (die in milliseconden doorgegeven moeten worden).
export const useGetCoordinates = (query: string): UseSuspenseQueryResult<Coordinates, Error> => {
return useSuspenseQuery({
queryKey: ['geocode', query],
queryFn: () => getCoordinates(query),
staleTime: Infinity,
gcTime: 1000 * 60 * 10,
})
}Onderstaande video demonstreert dat er nu slechts één request verstuurd wordt wanneer we een reeds gebruikte stad opnieuw gebruiken.
Refetching
Een weerbericht kan zeer snel wijzigen, daarom gebruiken we de refetchInterval property om het weerbericht elke 5 minuten opnieuw op te halen. Ondanks dat de staleTime standaard 0 is, wordt data toch niet automatisch opnieuw opgevraagd. Dit gebeurt enkel als de component terug gemount wordt of als het tabblad terug in focus komt. In de geval dat iemand de pagina open heeft staan op een deel van een display, maar er verder niet aankomt en met andere software bezig is, is het dus interessant om het weer automatisch te verversen. De refetchInterval optie verwacht een waarde in milliseconden.
export const useGetWeather = (coordinates: Coordinates): UseSuspenseQueryResult<IFiveDayForecast, Error> => {
return useSuspenseQuery({
queryKey: ['weather', coordinates],
queryFn: () => getWeather(coordinates),
refetchInterval: 5 * 60 * 1000,
})
}Error boundaries
We hebben tot nu toe nog geen rekening gehouden met fouten in de API calls. Om deze af te handelen maken we gebruik van error boundaries, een feature die automatisch geactiveerd wordt als we de useSuspenseQuery hook gebruiken.
Begrip: Error Boundary
Een error boundary is een component die, net als suspense, rond een stuk code gezet wordt en foutmeldingen opvangt. Net als bij suspense kan je een fallback component tonen wanneer er een fout optreedt.
Er bestaat echter geen kant-en-klare ErrorBoundary component, zoals in de documentatie te lezen is moet je zelf een error boundary bouwen en dit gaat enkel via class components, een outdated manier om componenten te schrijven in React. Aangezien we in deze cursus enkel functionele componenten gebruiken kiezen we ervoor om een ErrorBoundary component te installeren via npm
pnpm add react-error-boundaryimport {ErrorBoundary} from 'react-error-boundary'
const SomeComponent: FunctionComponent = () => {
return (
<ErrorBoundary fallback={<SomeFallbackComponent/>}>
<Suspense fallback={<SomeFallbackComponent/>}>
<SomeComponentThatLoadsDataThroughALibraryThatSupportsSuspense/>
</Suspense>
</ErrorBoundary>
)
}We gebruiken hieronder de ErrorFallback component die voorzien is in de startbestanden.
const Home: FunctionComponent = () => {
const [city, setCity, debouncedCity] = useDebouncedState<string>('Geel')
return (
<>
<h2 className="text-5xl">Weerbericht</h2>
<div className="my-4">
...
</div>
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<Weather city={debouncedCity} />
</Suspense>
</ErrorBoundary>
</>
)
}Bovenstaande code leidt tenslotte tot onderstaand resultaat. Merk op dan je in de console ziet dat dezelfde error meerdere keren voorkomt. Dit is te verwachten omdat TanStack Query een request standaard verschillende keren probeert voordat het definitief als mislukt beschouwd wordt. Hoeveel keer dit gebeurd is afhankelijk van de configuratie van de queryClient en useSuspenseQuery hooks.
Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
Een triple is een array met drie elementen. ↩︎
Een paar is een array met twee elementen. ↩︎
Naast de useSuspenseQuery hook bestaat ook de useQuery. De werking is grotendeels gelijk, met twee belangrijke verschillen. De useQuery hook gebruikt geen suspense of error boundary, dit betekent dat je zelf loading-indicators en foutmeldingen moet tonen via if-else structuren. Daarnaast zijn de typings voor de useSuspenseQuery beter, omwille van de manier waarop suspense werkt, is het type van de data property
T | undefinedbij useQuery in de plaats vanTbij een useSuspenseQuery. ↩︎