From ee1c27ae694a1ab1998e8456a88cb2b3c2dc3bc5 Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Mon, 14 Sep 2026 11:46:16 +0200 Subject: [PATCH] fix(src): vis konkrete API-feil og behold data ved feilet ny henting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gjør nettverksfeil, HTTP-feil, ugyldig JSON og feil format om til norske feilmeldinger, og vis dem i feilsiden - Vis feilsiden bare når det ikke finnes data fra før - Dokumenter cache-, refetch- og retry-valg, feilhåndtering og kontroller av nettverkskall i README Co-Authored-By: Birk --- README.md | 179 ++++++++++++++++++++++++++------------ src/App.tsx | 9 +- src/api/countries.ts | 32 ++++++- src/hooks/useCountries.ts | 2 + 4 files changed, 161 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 453838f..9ae84f5 100644 --- a/README.md +++ b/README.md @@ -172,21 +172,68 @@ API-et, for eksempel hovedstad eller areal, og er derfor valgfrie i typen. cachen ikke nullstilles når komponenter rendres på nytt. - `useCountries` bruker den faste query key-en `['countries']`. Alle komponenter som bruker hooken, deler dermed samme data og forespørsel. -- `fetchCountries` sjekker `response.ok` og kaster en feil ved - HTTP-feil, for eksempel 429 eller 500. TanStack Query sender med et - `AbortSignal`, som sendes videre til `fetch`. +- TanStack Query sender med et `AbortSignal`, som `fetchCountries` + sender videre til `fetch`. - `staleTime` er én time. Landdata endres sjelden, og API-et oppgir selv `Cache-Control: max-age=3600`. Så lenge dataene er ferske, gir nye rendringer og nye komponenter ingen nye kall. - `refetchOnWindowFocus: false` gjør at fanebytte ikke gir nye kall. -- `retry: 1` gir ett automatisk nytt forsøk. Deretter vises en - feilmelding med knappen «Prøv igjen», som kaller `refetch`. +- `refetchOnReconnect` og `refetchOnMount` har standardverdien `true`, + men gjelder bare data som er eldre enn `staleTime`. Kommer nettet + tilbake etter mer enn én time, hentes dataene på nytt. Ellers skjer + ingenting. `App` monteres bare én gang, så `refetchOnMount` gir ingen + ekstra kall. +- `retry: 1` gir ett automatisk nytt forsøk, også når brukeren trykker + «Prøv igjen». Deretter vises feilmeldingen. Flere forsøk ville bare + gjort feilmeldingen tregere, og brukeren kan prøve igjen selv. +- `gcTime` har standardverdien fem minutter. Den har ingen praktisk + betydning her, fordi `App` alltid bruker queryen og dataene derfor + aldri fjernes fra cachen mens appen er åpen. - Cachen ligger bare i minnet og forsvinner ved reload. Da hentes dataene på nytt, men nettleseren kan bruke sin egen HTTP-cache. - -React Strict Mode er beholdt. I utviklingsmodus monterer Strict Mode -komponentene to ganger. Nettverksfanen kan derfor vise én avbrutt og én -fullført forespørsel. I produksjonsbygget gjøres én forespørsel. +- Sortering, favoritter, favorittfilter, blaing og valg av land skjer + lokalt på dataene som allerede er hentet. De endrer ikke query key-en + og gir derfor ingen nye kall. + +### Feilhåndtering + +`fetchCountries` gjør feil om til norske meldinger. `App` viser +«Kunne ikke hente landdata.» etterfulgt av meldingen, og en knapp +«Prøv igjen» som kaller `refetch`. + +| Situasjon | Melding | +| ------------------------------------------ | ------------------------------------------------------- | +| Ingen kontakt (nettverks- eller CORS-feil) | «Fikk ikke kontakt med API-et. Sjekk nettforbindelsen.» | +| HTTP-feil, for eksempel 429 eller 500 | «API-et svarte med en feil (HTTP 500).» | +| Svaret er ikke gyldig JSON | «Svaret fra API-et var ikke gyldig JSON.» | +| Svaret er ikke en array | «Svaret fra API-et hadde et uventet format.» | + +- Den opprinnelige feilen legges ved som `cause`, slik at den kan + undersøkes ved feilsøking. +- En forespørsel som avbrytes via `AbortSignal`, sendes videre uendret. + TanStack Query håndterer den selv, og den vises ikke som feil. +- **Tomt svar:** `[]` gir «Fant ingen land.». +- **Manglende felter:** Land uten kode eller navn hoppes over. Andre + felter som mangler eller har feil type, blir `undefined`, og kortet + viser «Ikke oppgitt». +- **Feil etter vellykket henting:** Feiler en senere henting i + bakgrunnen, for eksempel ved gjenoppkobling, beholder TanStack Query + de gamle dataene. `App` viser derfor feilsiden bare når det ikke finnes + data, slik at brukeren kan fortsette med landene som allerede er hentet. + +### Utviklingsmodus og produksjonsbygg + +React Strict Mode er beholdt. I utviklingsmodus (`npm run dev`) monterer +Strict Mode komponentene to ganger. Første forespørsel avbrytes da, og en +ny startes. Nettverksfanen viser derfor én avbrutt og én fullført +forespørsel ved oppstart. Dette er forventet og skjer bare i utvikling. +Produksjonsbygget (`npm run build` og `npm run preview`) gjør én +forespørsel. Flere kall i produksjonsbygget ville vært et problem. + +### API-nøkler + +countries.dev krever ingen API-nøkkel. Repoet og klientkoden inneholder +ingen nøkler, tokens eller `.env`-filer. ### Landkort @@ -504,11 +551,59 @@ Kontrollene ble kjørt i utviklingsserveren (Vite) i nettleseren: - Sidemenyen: valg av land, markering av valgt land og favoritter, oppsett på smal skjerm og bruk med tastatur. +### Kontroller av API-kall og feiltilstander + +Kontrollene ble kjørt i headless Chrome med Chrome DevTools Protocol mot +produksjonsbygget (`npm run preview`) og utviklingsserveren +(`npm run dev`). API-kall ble telt i nettverkshendelsene. Feilsvar ble +simulert ved å avskjære forespørselen til countries.dev. Skriptene er +ikke lagt i repoet. + +- `npm run lint`, `npm run build` og `prettier --check` for de endrede + filene besto. +- **Ingen nøkler:** Søk i `src`, `index.html`, `vite.config.ts` og + sporede filer ga ingen API-nøkler, tokens eller `.env`-filer. +- **Nettverkskall i produksjonsbygget:** Totalt 1 kall til + `countries.dev/countries` gjennom hele økten. Tallet var uendret etter + hvert av disse stegene: + - Blaing med Neste, Neste og Forrige. + - Direktevalg i menyen. + - Sortering etter befolkning og tilbake til navn. + - Favorittendring for to land og favorittfilter av og på. + - Rerender via endret vindusstørrelse, og fokus- og + `visibilitychange`-hendelser. + - Nettet av og på igjen mens dataene var ferske. + + Etter reload ble det gjort 1 nytt kall, som forventet. + +- **Utviklingsserveren:** Samme steg ga 1 avbrutt og 1 fullført kall ved + oppstart, og ingen flere. Dette er Strict Mode, se + [Utviklingsmodus og produksjonsbygg](#utviklingsmodus-og-produksjonsbygg). +- **Feiltilstander (produksjonsbygg):** + - HTTP 500, HTTP 429, ugyldig JSON, et objekt i stedet for array og + nettverksfeil ga hver sin melding fra tabellen under + [Feilhåndtering](#feilhåndtering), med knappen «Prøv igjen». Hvert + tilfelle ga 2 kall (første forsøk og ett nytt forsøk). + - `[]` ga «Fant ingen land.». + - Land med manglende felter og felter med feil type ga ingen krasj. + Land uten kode ble hoppet over («Land 1 av 2»). +- **«Prøv igjen»:** Etter nettverksfeil viste klikket «Laster land …» og + deretter landene fra det ekte API-et. Når nytt forsøk feilet igjen + (HTTP 503), ga klikket 2 nye kall, og feilmeldingen og knappen kom + tilbake. +- **Feil etter vellykket henting:** Kontrollert med `@tanstack/query-core` + i Node. Etter en vellykket henting og en feilet ny henting var + `isError` `true`, mens dataene fortsatt fantes. Før denne endringen + ville `App` derfor vist feilsiden. Nå vises dataene videre. +- Ingen JavaScript-feil eller konsollfeil i noen av kjøringene. + Automatiske tester som gjenstår når Vitest er satt opp: -- `fetchCountries`: vellykket henting, HTTP-feil, uventet format, tom - respons og manglende felter, med mocket `fetch`. -- `App`: lastetilstand, feilmelding, «Prøv igjen» og tomt resultat. +- `fetchCountries`: vellykket henting, HTTP-feil, nettverksfeil, ugyldig + JSON, uventet format, tom respons, manglende felter og avbrutt + forespørsel, med mocket `fetch`. +- `App`: lastetilstand, feilmelding med riktig tekst, «Prøv igjen», tomt + resultat, og at eksisterende data vises når en ny henting feiler. - `CountryCard`: fullstendige data, manglende felter og flagg som ikke kan lastes. - `CountryNavigation`: forrige/neste gir riktig land, knappene er @@ -542,46 +637,20 @@ Automatiske tester som gjenstår når Vitest er satt opp: ChatGPT ble brukt som støtte til prosjektplanlegging og veiledning for oppsett av Vite, React, TypeScript, ESLint og Prettier. -Claude Code (Anthropic) ble brukt til API-integrasjonen med TanStack -Query. Claude Code gjorde dette: - -- Undersøkte dokumentasjonen og faktiske svar fra countries.dev. -- Foreslo datautvalget og installerte `@tanstack/react-query`. -- Skrev `src/types/country.ts`, `src/api/countries.ts`, - `src/hooks/useCountries.ts` og `src/components/CountryList.tsx`. -- Endret `src/main.tsx`, `src/App.tsx` og `src/App.css`. -- Kjørte kontrollene beskrevet under - [Kontroller av API-integrasjonen](#kontroller-av-api-integrasjonen). -- Skrev README-avsnittene om API-integrasjonen. - -Claude Code ble også brukt til landkortet (issue #7). Claude Code -skrev `src/components/CountryCard.tsx` og `CountryCard.css`, flyttet -lasting og feilhåndtering fra `CountryList` til `src/App.tsx`, fjernet -`CountryList.tsx`, kjørte kontrollene under -[Kontroller av landkortet](#kontroller-av-landkortet) og oppdaterte -README. - -Claude Code ble også brukt til navigasjonen (issue #8). Claude Code -skrev `src/components/CountryNavigation.tsx` og `CountryNavigation.css`, -la til valgt landkode som state i `src/App.tsx`, kjørte lint, formatering -og bygg, og oppdaterte README. - -Claude Code ble også brukt til sorteringen (issue #9). Claude Code -skrev `src/utils/sortCountries.ts`, `src/utils/sortOrderStorage.ts`, -`src/hooks/useSortOrder.ts`, `src/components/SortControl.tsx` og -`SortControl.css`, koblet sorteringen inn i `src/App.tsx`, kjørte -kontrollene under -[Kontroller av sorteringen](#kontroller-av-sorteringen) og oppdaterte -README. - -Claude Code ble også brukt til favorittene (issue #10). Claude Code -skrev `src/utils/favoritesStorage.ts`, `src/hooks/useFavorites.ts`, -`src/components/FavoriteButton.tsx` og `FavoriteButton.css`, la til -plass for knappen i `CountryCard`, koblet favorittene inn i -`src/App.tsx`, kjørte kontrollene under -[Kontroller av favorittene](#kontroller-av-favorittene) og oppdaterte -README. - -Endringene skal gjennomgås av gruppen i pull requesten før de merges. -Dokumentasjonen oppdateres med videre KI-bruk og hvordan forslag er -kontrollert av gruppen. +Claude Code (Anthropic) ble brukt gjennom prosjektet, i tre roller: + +- **Kodeutkast:** Claude Code laget førsteutkast til kode for + issues: landkortet (#7), favorittene (#10), favorittfilteret og sidemenyen + (#11) og feilhåndteringen (#16). Gruppen bestemte hva som skulle + lages, gjennomgikk utkastene i pull requests og endret der løsningen ikke passet. Et eksempel er at landvalget ble flyttet fra en + nedtrekksliste til en sidemeny. +- **Sparringspartner:** Vi brukte Claude Code til å diskutere + løsningsvalg, for eksempel valg av API etter at REST Countries krevde + autentisering, cache- og refetch-innstillinger i TanStack Query, og + hvordan valgt land skal oppføre seg ved sortering og filtrering. +- **Kontroll og dokumentasjon:** Claude Code verifiserte API-svar, + kjørte lint, formatering og bygg, kontrollerte nettverkskall og + feiltilstander i nettleseren, og hjalp med å skrive README. + +Alle endringer er gjennomgått av et annet gruppemedlem i pull request +før merge. diff --git a/src/App.tsx b/src/App.tsx index 85cacdf..1a5e14a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ import { sortCountries, type SortOrder } from './utils/sortCountries'; import './App.css'; function App() { - const { data, isPending, isError, refetch } = useCountries(); + const { data, error, isPending, refetch } = useCountries(); // Landkoden til valgt land. null betyr at brukeren ikke har valgt noe ennå. const [selectedCode, setSelectedCode] = useState(null); // Valgt sortering, lagret i sessionStorage. @@ -62,10 +62,13 @@ function App() { // Ved «Prøv igjen» går queryen tilbake til pending, så lastetilstanden vises igjen. if (isPending) { content =

Laster land …

; - } else if (isError) { + } else if (!data) { + // Feilsiden vises bare når det ikke finnes data. Feiler en senere henting + // i bakgrunnen, beholder TanStack Query de gamle dataene, og appen + // fortsetter å vise dem i stedet for å bytte til feilsiden. content = (
-

Kunne ikke hente landdata. Sjekk nettforbindelsen og prøv igjen.

+

Kunne ikke hente landdata. {error?.message}

diff --git a/src/api/countries.ts b/src/api/countries.ts index f503826..1dca687 100644 --- a/src/api/countries.ts +++ b/src/api/countries.ts @@ -13,14 +13,40 @@ const params = new URLSearchParams({ limit: String(COUNTRY_LIMIT), }); +// Feilmeldingene vises for brukeren, så de er skrevet på norsk. export async function fetchCountries(signal?: AbortSignal): Promise { - const response = await fetch(`${API_URL}?${params.toString()}`, { signal }); + let response: Response; + + try { + response = await fetch(`${API_URL}?${params.toString()}`, { signal }); + } catch (error) { + // En avbrutt forespørsel er ikke en feil for brukeren. TanStack Query + // håndterer den selv, så den sendes videre uendret. + if (signal?.aborted) { + throw error; + } + // Den opprinnelige feilen legges ved som cause, nyttig ved feilsøking. + throw new Error('Fikk ikke kontakt med API-et. Sjekk nettforbindelsen.', { + cause: error, + }); + } if (!response.ok) { - throw new Error(`Serveren svarte med HTTP ${response.status}.`); + throw new Error(`API-et svarte med en feil (HTTP ${response.status}).`); } - const data: unknown = await response.json(); + let data: unknown; + + try { + data = await response.json(); + } catch (error) { + if (signal?.aborted) { + throw error; + } + throw new Error('Svaret fra API-et var ikke gyldig JSON.', { + cause: error, + }); + } if (!Array.isArray(data)) { throw new Error('Svaret fra API-et hadde et uventet format.'); diff --git a/src/hooks/useCountries.ts b/src/hooks/useCountries.ts index 3d78e0c..4d540eb 100644 --- a/src/hooks/useCountries.ts +++ b/src/hooks/useCountries.ts @@ -13,6 +13,8 @@ export function useCountries() { staleTime: 60 * 60 * 1000, // Unngå ny henting bare fordi brukeren bytter fane eller vindu. refetchOnWindowFocus: false, + // refetchOnReconnect beholder standardverdien: når nettet kommer tilbake, + // hentes dataene på nytt bare hvis de er eldre enn staleTime. // Ett automatisk nytt forsøk. Deretter vises feilmeldingen, // og brukeren kan prøve igjen selv. retry: 1,