React har ændret den måde, vi arbejder med komponenter på. Tidligere var det kun klassekomponenter, der kunne håndtere tilstand og livscyklusmetoder, men med introduktionen af Hooks i React kan vi nu håndtere disse funktionaliteter i funktionelle komponenter. Dette gør funktionelle komponenter mere kraftfulde og lettere at skrive og forstå.

Hooks er funktioner, der gør det muligt at "tilslutte sig" Reacts interne funktioner som tilstandsstyring, kontekst, effekter og meget mere. Hooks præfikseres med "use"-nøgleordet, som i useState, useEffect, useContext og så videre. React leverer flere indbyggede Hooks, men det er også muligt at oprette brugerdefinerede Hooks for at kapsle genanvendelig tilstandslogik.

De mest brugte indbyggede Hooks er useState, useEffect, og useContext. Foruden disse findes useCallback og useMemo, som anvendes til performanceoptimering.

useState: Hvordan håndterer vi tilstand i funktionelle komponenter?

Den første Hook, vi støder på, når vi arbejder med React, er useState. Denne Hook gør det muligt at tilføje tilstand til funktionelle komponenter, hvilket tidligere kun var muligt i klassekomponenter.

Når en komponent først renderes, kan den have nogle standardværdier for sin tilstand. Disse værdier kaldes "initial tilstand", og de kan sættes med useState. Et eksempel på, hvordan vi bruger useState til at håndtere tilstand, kunne være som følger:

javascript
export default function App() {
const [name] = React.useState("Mike");
const [age] = React.useState(32);
return ( <> My name is {name} My age is {age} </> ); }

I eksemplet her har vi to tilstandsværdier, name og age. Begge initialiseres med en værdi ved hjælp af useState. Denne Hook returnerer et array, hvor den første værdi er den nuværende tilstand, og den anden værdi er en funktion, der opdaterer tilstanden.

Opdatering af tilstand med useState

Når vi arbejder med tilstand, vil værdierne ofte ændre sig, for eksempel som resultat af brugerinput eller data, der hentes fra en API. useState giver os en funktion til at opdatere tilstanden for hver tilstandsværdi.

Lad os se på, hvordan vi opdaterer værdierne for name og age ved hjælp af inputfelter:

javascript
function App() {
const [name, setName] = React.useState("Mike");
const [age, setAge] = React.useState(32);
return ( <> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> My name is {name} <input type="number" value={age} onChange={(e) => setAge(e.target.value)} /> My age is {age} </> ); }

I dette eksempel har vi tilføjet inputfelter, hvor brugeren kan ændre værdierne af name og age. Hver gang brugeren ændrer værdien i et inputfelt, bliver den tilknyttede funktion (setName eller setAge) kaldt med den nye værdi som argument. Når tilstanden opdateres, vil komponenten blive genrendret med de nye værdier.

useEffect: Initialisering og oprydning i komponenter

Ofte har vores komponenter brug for at udføre handlinger, når de oprettes. Dette kunne f.eks. være at hente data fra en API eller oprette abonnenter på eksterne begivenheder. En almindelig praksis er at bruge useEffect-hooken til at håndtere disse situationer.

useEffect køres som standard efter hver render, og det bruges til at håndtere sideeffekter i React-komponenter. Vi kan også bruge useEffect til at rense op, når komponenten fjernes. Et eksempel på, hvordan useEffect kan bruges til at hente data, ser således ud:

javascript
import React, { useEffect, useState } from 'react'; function App() { const [data, setData] = useState(null); useEffect(() => { fetch('https://api.example.com/data') .then(response => response.json()) .then(data => setData(data)); }, []); // Tom array sikrer, at effekten kun kører én gang ved komponentens første render return ( <div>
{data ? <p>{data}</p> : <p>Loading...</p>}
</div> ); }

I eksemplet hentes data fra en API, og når dataene er modtaget, opdateres tilstanden med setData. Denne effekt vil kun blive udført én gang, da den tomme array som andet argument til useEffect sørger for, at effekten kun kører ved den første render.

useEffect kan også bruges til oprydning, når komponenten fjernes fra DOM’en. Hvis vi f.eks. opretter en abonnent, kan vi bruge useEffect til at rydde op ved at afmelde abonnenten:

javascript
useEffect(() => {
const subscription = subscribeToSomething(); return () => { subscription.unsubscribe(); }; }, []);

Vigtige overvejelser ved brug af Hooks

Når du arbejder med React-hooks, er det vigtigt at være opmærksom på, hvordan og hvornår de kører. For eksempel er det essentielt at forstå, at useEffect ikke kører synkront, men efter DOM’en er blevet opdateret. Dette betyder, at ændringer, der sker inde i useEffect, ikke vil påvirke den aktuelle render, hvilket kan have betydning, når du arbejder med sideeffekter.

En anden vigtig ting er, at hooks kun må kaldes i funktionelle komponenter eller brugerdefinerede hooks. De må ikke kaldes betinget eller inde i loops, da dette kan føre til uforudsigelige resultater og fejl i komponentens opførsel.

Yderligere anbefalinger

Når du arbejder med useState, er det en god idé at holde tilstandsopdateringer enkle og præcise. Brug en separat useState for hver tilstandsværdi i stedet for at samle dem i objekter, da dette gør koden lettere at forstå og vedligeholde.

For komplekse eller dynamiske data, som kan kræve optimering, kan du overveje at bruge useMemo eller useCallback. Disse hooks hjælper med at forhindre unødvendige beregninger og re-renders, hvilket kan forbedre ydeevnen i din applikation.

Hvordan React Håndterer Begivenheder: Fra SyntheticEvents til Effektiv Hukommelsesstyring

Når en begivenhedshåndtering tilknyttes et DOM-element ved hjælp af den native addEventListener funktion, vil en callback få en begivenhedsargument sendt til sig. I React er begivenhedshåndteringsfunktioner også udstyret med et begivenhedsargument, men dette er ikke den standardiserede begivenhedsinstans. I stedet bruges en såkaldt SyntheticEvent, som fungerer som et simpelt omslag for de native begivenhedsinstanser. Formålet med SyntheticEvent er todelt: For det første skaber det en ensartet begivenhedsinterface, der normaliserer browserinkonsistenser. For det andet indeholder det de nødvendige oplysninger for at sikre, at begivenhedspropagering fungerer korrekt.

Når et DOM-element, der er en del af en React-komponent, udløser en begivenhed, håndterer React begivenheden ved at sætte sine egne lyttere op. Derefter oprettes enten en ny SyntheticEvent, eller en eksisterende instans hentes fra en intern pulje, afhængig af tilgængeligheden. Hvis der er nogen begivenhedshåndterere knyttet til komponenten, der matcher den udløste DOM-begivenhed, vil de blive kørt med den pågældende SyntheticEvent sendt til dem.

Begivenhedsobjektet i React indeholder egenskaber og metoder, der ligner dem i de native JavaScript-begivenheder. Du kan få adgang til egenskaber som event.target for at hente DOM-elementet, der udløste begivenheden, eller event.currentTarget, som henviser til det element, begivenhedshåndtereren er knyttet til. Derudover tilbyder begivenhedsobjektet metoder som event.preventDefault() for at forhindre standardadfærden, som typisk vil være formindsendelse eller linkklik, samt event.stopPropagation() for at stoppe begivenheden i at sprede sig op gennem komponenttræet, hvilket forhindrer begivenhedsopblødning.

I modsætning til den traditionelle tilgang til begivenhedshåndtering, hvor begivenheder bobler op gennem DOM-træet og udløser håndterere på forfader-elementer, er begivenhedspropagering i React baseret på komponenthierarkiet i stedet for DOM-hierarkiet. Når en begivenhed opstår i en underordnet komponent, opfanger React begivenheden ved roden af komponenttræet og arbejder sig ned til den specifikke komponent, der udløste begivenheden. Denne tilgang, kaldet begivenhedsdelegation, forenkler håndteringen af begivenheder ved at centralisere logikken ved roden af komponenttræet.

Fordelene ved Reacts begivenhedsdelegation er flere. For det første reduceres antallet af begivenhedslyttere tilknyttet individuelle DOM-elementer, hvilket forbedrer performance. For det andet muliggør det håndtering af begivenheder for dynamisk oprettede eller fjernede elementer uden at skulle bekymre sig om manuelt at tilknytte eller fjerne begivenhedslyttere.

React håndterer begivenheder effektivt ved at bruge en begivenhedspulje. Hver gang en begivenhed udløses, tages en instans fra puljen, og dens egenskaber bliver opdateret. Når begivenhedshåndtereren er færdig med at køre, returneres den syntetiske begivenhed til puljen, som illustreret i diagrammet. Dette forhindrer, at garbage collector ofte bliver kaldt, når mange begivenheder bliver udløst. Puljen holder referencer til de syntetiske begivenhedsinstanser, så de aldrig bliver kandidater til affaldsindsamling. Dette sikrer, at React aldrig behøver at allokere nye instanser.

Imidlertid er der en vigtig forbehold, som du bør være opmærksom på. Hvis du tilgår syntetiske begivenhedsinstanser fra asynkront kode i dine begivenhedshåndterere, opstår der problemer. Når en begivenhedshåndtering er afsluttet, og den syntetiske begivenhed returneres til puljen, bliver alle dens egenskaber tømt. Dette kan medføre uventede adfærdsmønstre og fejl, som f.eks. undefined værdier, som illustreret i det følgende eksempel:

javascript
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => { resolve(); }, 1000); }); } function MyButton(props) { function onClick(e) { console.log("clicked", e.currentTarget.style); fetchData().then(() => { console.log("callback", e.currentTarget.style); // Kan resultere i en fejl }); } return <button onClick={onClick}>{props.children}</button>; }

I dette tilfælde forsøger den anden console.log at tilgå en syntetisk begivenhedsegenskab i en asynkron callback, der kører efter, at begivenhedshåndtereren er afsluttet. Når begivenheden returneres til puljen, tømmes dens egenskaber, hvilket resulterer i en advarsel og en undefined værdi.

Denne problemstilling understreger nødvendigheden af at undgå at tilgå syntetiske begivenheder i asynkrone koder, da React håndterer begivenhedspoolning og livscyklus, hvilket betyder, at når begivenhedshåndtereren afsluttes, er begivenheden ikke længere tilgængelig.

Det er vigtigt at forstå, at Reacts måde at håndtere begivenheder på, med dets syntetiske begivenheder og poolning, giver en effektiv og mindsket belastning på hukommelsen, men samtidig kræver det, at udviklere skriver kode, der undgår asynkrone operationer på begivenhedsobjekter. Det er en essentiel praksis at være opmærksom på disse detaljer for at opnå både effektivitet og stabilitet i applikationen.

Hvordan man tester funktioner i JavaScript med Vitest

Unit tests er essentielle for at sikre, at vores kode fungerer som forventet. Når vi arbejder med JavaScript og React, er der mange måder at teste funktioner på, især når de interagerer med eksterne systemer som API'er eller tidsstyring. I denne sektion vil vi udforske, hvordan man kan skrive effektive unit tests ved at bruge Vitest, et populært testværktøj for JavaScript.

Lad os begynde med en simpel funktion, som kan bruges til at demonstrere et typisk testscenario. Forestil dig, at du har en funktion, der kalder en onSelect callback tre gange:

javascript
onSelect('1'); onSelect('2'); onSelect('3');

Denne funktion kunne være en del af et selector-komponent, der reagerer på brugerinput. I et testmiljø ønsker vi at sikre os, at denne callback bliver kaldt præcist tre gange og med de rigtige argumenter. Vi kan skrive en test, der bruger Vitests spy- og mock-funktioner til at verificere dette:

javascript
test('selector', () => { const onSelect = vi.fn(); selector(onSelect); expect(onSelect).toBeCalledTimes(3); expect(onSelect).toHaveBeenLastCalledWith('3'); });

I testen ovenfor bruger vi vi.fn(), som opretter en mock-funktion, der kan spore antal kald og argumenter. Derefter tester vi, at onSelect bliver kaldt præcist tre gange og at det sidste kald var med argumentet '3'. Vi kunne også bruge metoden toHaveBeenCalledWith for at sikre, at callbacken blev kaldt med de rigtige argumenter på hvert kald:

javascript
expect(onSelect).toHaveBeenCalledWith('1'); expect(onSelect).toHaveBeenCalledWith('2'); expect(onSelect).toHaveBeenCalledWith('3');

En anden nyttig funktion i Vitest er vi.spyOn(), som gør det muligt at "spionere" på en eksisterende funktion. Denne funktion er praktisk, når du vil teste en funktion, der allerede eksisterer i et objekt. Her er et eksempel på, hvordan man bruger spyOn:

javascript
test('spyOn', () => { const cart = { getProducts: () => 10, }; const spy = vi.spyOn(cart, 'getProducts');
expect(cart.getProducts()).toBe(10);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveReturnedWith(10); });

I dette eksempel spionerer vi på getProducts-metoden i cart-objektet. Vi kan derefter kontrollere, om metoden blev kaldt, og om den returnerede den forventede værdi. Vitest tilbyder også metoden toHaveReturnedWith, som gør det muligt at verificere, hvad en funktion returnerede.

Når vi tester funktioner, der har side effects eller afhænger af eksterne systemer, bliver testen lidt mere kompleks. En funktion, der f.eks. interagerer med systemets tid eller laver netværksanmodninger, kræver særlige teknikker for at kunne testes effektivt. I sådanne tilfælde kan vi bruge mocking til at erstatte ekstern adfærd med foruddefinerede værdier.

Lad os tage et eksempel på funktioner, der arbejder med timers. Hvis en funktion afventer en forsinkelse på f.eks. 60 sekunder, kan det være tidskrævende at teste den ved at vente i realtid. I stedet kan vi bruge Vitests fake timer-funktionalitet til at simulere tid og sikre, at funktionerne opfører sig som forventet:

javascript
function executeInMinute(func: () => void) {
setTimeout(func, 1000 * 60); } function executeEveryMinute(func: () => void) { setInterval(func, 1000 * 60); } const mock = vi.fn(() => console.log('done')); describe('delayed execution', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should execute the function', () => { executeInMinute(mock); vi.runAllTimers(); expect(mock).toHaveBeenCalledTimes(1); }); it('should not execute the function', () => { executeInMinute(mock); vi.advanceTimersByTime(2); expect(mock).not.toHaveBeenCalled(); }); it('should execute every minute', () => { executeEveryMinute(mock); vi.advanceTimersToNextTimer(); expect(mock).toHaveBeenCalledTimes(1); vi.advanceTimersToNextTimer(); expect(mock).toHaveBeenCalledTimes(2); }); });

I dette eksempel bruger vi vi.useFakeTimers() til at aktivere fake-timerne og vi.runAllTimers() til at køre alle timers med det samme. Vi bruger også vi.advanceTimersByTime() og vi.advanceTimersToNextTimer() til at simulere tid og kontrollere, at funktionerne bliver kaldt på de rigtige tidspunkter.

En af de mere udfordrende opgaver i unit testing er at teste funktioner, der afhænger af eksterne data eller systemer, som vi ikke kan kontrollere under testen. For eksempel, hvis vi har en funktion, der afhænger af data fra en server eller enhedens sensor, kan vi bruge mocking til at erstatte denne ekstern afhængighed med en foruddefineret værdi.

For at demonstrere mocking, lad os antage, at vi arbejder med en funktion, der returnerer antallet af skridt fra en enheds sundhedssystem (f.eks. en smartphone):

javascript
export function getSteps() { // SOME NATIVE LOGIC return 100; }

I virkeligheden ville denne funktion interagere med et natursystem som f.eks. et sundhedssystem API. For at teste denne funktion kan vi mocke dens adfærd:

javascript
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { getSteps } from './ios-health-kit'; describe('IOS Health Kit', () => { beforeAll(() => { vi.mock('./ios-health-kit', () => ({ getSteps: vi.fn().mockImplementation(() => 2000), })); }); it('should return steps', () => {
expect(getSteps()).toBe(2000);
expect(getSteps).toHaveBeenCalled(); }); });

Her mocker vi getSteps-funktionen, så den altid returnerer 2000, uanset hvad den faktiske funktion ville gøre. Dette gør det muligt at teste, hvordan den funktion, der afhænger af getSteps, vil opføre sig i en kontrolleret testmiljø.

En vigtig pointe at bemærke er, at selvom det kan virke som om vi kun tester "simple" funktioner, er de teknikker, vi diskuterer her, grundlæggende for at kunne teste mere komplekse interaktioner og komponenter i real-life applikationer. Dette er især relevant i tilfælde, hvor en funktion er afhængig af eksterne systemer eller komplekse tidsrelaterede operationer.

Hvordan Fleksible Rækker og Gitter Opbygger Dynamiske Layouts i React Native

Når du arbejder med React Native, er fleksible layoutmekanismer som Flexbox afgørende for at skabe responsive og dynamiske brugergrænseflader. Flexbox gør det muligt at arrangere elementer på en måde, der automatisk tilpasser sig forskellige skærmstørrelser og orienteringer. Dette afsnit udforsker, hvordan man opbygger fleksible rækker og gitter, og hvordan disse layouts kan fungere både i portræt- og landskabsorientering.

I et fleksibelt layout, hvor du bruger justifyContent med værdien space-around, fordeles rummet proportionalt omkring og imellem de enkelte sektioner. Denne metode gør det nemt at bygge en række, der flyder fra venstre mod højre indtil slutningen af skærmen, hvorefter nye elementer automatisk bliver placeret på en ny række. Dette betyder, at du ikke behøver at vide på forhånd, hvor mange kolonner der er i en given række. I stedet bestemmes dimensionerne af hvert barns størrelse, hvad der kan passe ind i rækken.

Et fleksibelt gitter er ideelt, når du har flere sektioner, som skal have ens bredde og højde, men hvor antallet af sektioner er ukendt. Ved at bruge Flexbox kan du skabe et layout, hvor elementerne kontinuerligt tilføjes fra venstre mod højre og automatisk flyder til næste række, når pladsen er opbrugt. Dette betyder, at layoutet forbliver fleksibelt og kan håndtere forskellige mængder indhold uden at kræve statisk styring af antallet af sektioner.

Et klassisk eksempel på et fleksibelt gitter kan ses i portræt- og landskabsorientering, hvor layoutet automatisk tilpasser sig. I portrætorientering er layoutet for det meste komprimeret, mens det i landskabsorientering kan give et overskydende tomrum på højre side, som kan afhjælpes ved at justere marginerne og bredden af de enkelte komponenter.

Når du arbejder med fleksible rækker og kolonner, åbnes en ny dimension af layoutmuligheder, der giver dig mulighed for at nestede kolonner i rækker og omvendt. Dette skaber en mere kompleks struktur, hvor elementer kan være indlejret i hinanden, hvilket er særligt nyttigt i mere sofistikerede layouts. Dette er nøglen til at skabe brugerflader, der både er dynamiske og funktionelle, og som tilpasser sig forskellige enheder og skærmstørrelser.

En vigtig komponent i denne fleksible tilgang er den rene og strukturerede JSX, som opnås ved at opdele rækker og kolonner i separate komponenter. For eksempel kan en Row-komponent og en Column-komponent anvendes til at håndtere de forskellige layoutdynamikker. Begge komponenter anvender specifikke stilarter, der gør det muligt at ændre strukturen af layoutet uden at ændre den overordnede logik for, hvordan indholdet vises på skærmen.

Fleksible rækker og kolonner giver derfor ikke kun et æstetisk layout, men også en solid, genanvendelig struktur, der kan anvendes i mange forskellige typer applikationer. Flexbox som layoutværktøj forbliver et af de mest effektive måder at styre komponenternes position og størrelse på, samtidig med at du bevarer fuld kontrol over, hvordan de interagerer med hinanden på tværs af forskellige skærmstørrelser og orienteringer.

En vigtig pointe at huske er, at de fleksible gitter og rækker ikke kun skaber et visuelt tiltalende layout, men også muliggør bedre brugeroplevelse, da de gør applikationen mere responsiv og tilpasningsdygtig til de forskellige enheder, som appen kører på. I praksis betyder det, at applikationens flow kan forblive intuitivt og hurtigt, selv når brugeren ændrer skærmindstillingerne.

Det er også værd at bemærke, at for at undgå problemer med overskydende plads i layoutet, især i landskabsorientering, kan du finjustere elementernes dimensioner, marginer og størrelser. Dette kan hjælpe med at forhindre, at skærmen ser for tom ud, når der er mindre indhold at vise. En grundlæggende forståelse af, hvordan man arbejder med Flexbox, giver dig muligheden for at skabe brugervenlige grænseflader, der fungerer optimalt på alle skærmopløsninger og enheder.

Hvordan man navigerer mellem skærme i mobile apps med React Native og Expo Router

Når du bygger en mobilapplikation, er navigation en af de grundlæggende funktioner, der muliggør en god brugeroplevelse. I denne del vil vi undersøge, hvordan man effektivt kan implementere navigation i en React Native-applikation, herunder både tab-navigation og drawer-navigation, samt hvordan man kan bruge filbaseret routing med Expo Router til at definere navigationsstrukturen.

I en grundlæggende React Native-applikation vil de skærme, som udgør appen, blive oprettet som komponenter, der derefter kan videregives til din applikation for at generere navigationen. For eksempel kan vi skabe en simpel Home-komponent, der blot returnerer en grundlæggende besked:

jsx
export default function Home() {
return ( <div>Home Content</div> ); }

Når vi nu ser på, hvordan navigationen fungerer på en iOS-enhed, vil vi opdage, at der er en tab-navigation i bunden, hvor de tre skærme i appen er listet, og den aktive skærm er markeret. Brugeren kan derefter klikke på de øvrige tab-knapper for at navigere mellem skærmene. Dette er en enkel og intuitiv måde at implementere navigation på i mobilapps.

På Android ser navigationen anderledes ud. Her bliver en drawer-navigation brugt, som kan åbnes ved at swipe fra venstre side af skærmen. Når drawer'en er åben, vil brugeren kunne se knapper, der leder til de forskellige skærme i appen. Denne metode gør det muligt at have flere navigationsmuligheder på en kompakt måde, hvor pladsen på skærmen udnyttes optimalt.

Udover disse grundlæggende navigationsmetoder, kan vi også arbejde med filbaseret routing ved hjælp af Expo Router. Dette system minder meget om Next.js' routing og giver mulighed for at definere skærme blot ved at oprette nye filer i appens mapper. Når du arbejder med Expo Router, skal du blot oprette en ny fil i appens mappe, og skærmen vil blive tilføjet til din applikation. Dette giver en meget simpel og deklarativ måde at definere navigation på.

For eksempel, når du opretter en _layout.tsx-fil som rodkomponent, kan du definere, hvordan skærme skal arrangeres i appen:

jsx
import { Stack } from "expo-router";
export default function RootLayout() {
return ( <Stack.Screen name="Home" /> ); }

Herefter kan du oprette index.tsx, der indeholder Home-skærmen:

jsx
import { Link } from "expo-router";
export default function Home() {
return ( <div> Home Screen <Link href="/settings">Go to Settings</Link> </div> ); }

Dette gør det muligt for brugeren at navigere til Settings-skærmen ved at klikke på et link, ligesom man ville gøre på en webside. Den deklarative tilgang til at definere skærme gør det nemt at opbygge applikationer, og den URL-baserede navigation fungerer uden problemer. En stor fordel ved denne tilgang er, at dyb linking fungerer automatisk. Det betyder, at du kan åbne specifikke skærme direkte via app-links.

En af de primære fordele ved Expo Router er, at du kan håndtere navigationen på en måde, der er intuitiv og nem at forstå, især hvis du har erfaring med webudvikling, hvor du arbejder med URL'er og links. Det gør processen med at opbygge og vedligeholde appens navigation meget lettere, samtidig med at du bevarer fleksibiliteten i din kode.

I denne sektion har vi set på, hvordan du kan implementere både den traditionelle navigation med tab- og drawer-komponenter samt den nyere filbaserede navigation med Expo Router. Ved at forstå disse teknikker kan du effektivt bygge applikationer med kompleks navigation, der stadig er lette at vedligeholde og udvide. At bruge filbaseret routing giver en ekstra fordel, da det gør det lettere at skalere applikationen og tilføje nye skærme uden at skulle ændre meget i den eksisterende navigationsstruktur.

Endelig er det vigtigt at understrege, at en god brugeroplevelse i en mobilapplikation ikke kun afhænger af den funktionelle navigation, men også af, hvordan navigationen er integreret med appens øvrige funktioner. Navigationskomponenterne skal være intuitive, hurtige og responsive for at sikre, at brugerne kan interagere med appen uden at opleve forsinkelser eller forvirring. Desuden bør du altid overveje, hvordan brugere vil interagere med din app på forskellige enheder og skærmstørrelser for at sikre, at navigationen fungerer godt på tværs af platforme.