I React-applikasjoner håndteres hendelser på en unik måte som skiller seg fra tradisjonelle metoder som jQuery. I stedet for å bruke imperative koder for å koble hendelseshåndterere til DOM-elementer, benytter React en deklarativ tilnærming som gjør at hendelsene kan defineres direkte i JSX-strukturen. Dette gir en klar og intuitiv syntaks som gjør det enklere å forstå hvordan brukerinteraksjoner blir behandlet.

For eksempel, i en enkel React-komponent kan vi definere en hendelseshåndterer som knytter seg til en spesifikk hendelse, som en klikkhendelse, ved å bruke onClick-evnetegnet i JSX. Dette gjør koden lett å lese og vedlikeholde. I tillegg, ved å bruke hooks som useRef, kan vi manipulere DOM-elementer direkte uten å trigge unødvendige gjenrenderinger av komponentene, noe som kan bidra til å forbedre ytelsen i applikasjonen.

Når vi bruker Reacts mekanisme for hendelseshåndtering, kan vi definere flere hendelseshåndterere på samme element. Dette kan inkludere for eksempel både onChange og onBlur på et inputfelt, som demonstrert i eksemplet:

jsx
function MyInput() {
const onChange = () => { console.log("changed"); }; const onBlur = () => { console.log("blurred"); }; return <input onChange={onChange} onBlur={onBlur} />; }

Denne deklarative tilnærmingen gjør det enkelt å legge til flere hendelseshåndterere uten at koden blir rotete, noe som er en stor fordel når applikasjonen vokser i kompleksitet.

En annen viktig aspekt ved hendelseshåndtering i React er bruken av "syntetiske hendelser". Dette er en abstraksjon som React benytter for å håndtere hendelser på en mer effektiv måte. I stedet for å legge til individuelle hendelseslyttere på hvert DOM-element, bruker React én global hendelseslytter som fanger opp hendelser som bobler opp gjennom DOM-treet. Dette gir en betydelig ytelsesgevinst, da det reduserer overheaden ved å måtte registrere flere hendelseslyttere.

Når en hendelse skjer, som for eksempel et klikk på en knapp, "bobbler" hendelsen opp gjennom DOM-treet. React sjekker om det finnes noen håndterere som er registrert for denne hendelsen og kaller dem dersom de finnes. Denne prosessen gjør det mulig å opprettholde en ren adskillelse mellom UI-strukturene og DOM, og sikrer samtidig at hendelseshåndtering skjer på en effektiv og ytelsesvennlig måte.

I tillegg til å bruke syntetiske hendelser, kan vi også benytte memoizationteknikker i React for å unngå unødvendige beregninger og gjenrenderinger. Med hooks som useMemo, useCallback, og useRef kan vi sikre at verdier og referanser bevares mellom gjenrenderinger, og dermed redusere kompleksiteten i applikasjonen.

For å oppnå best mulig ytelse er det viktig å forstå at React fokuserer på å gjøre det enklere å jobbe med hendelser samtidig som det optimaliserer ressursbruken. Dette oppnås gjennom en kombinasjon av deklarative komponenter, syntetiske hendelser og memoization.

Ved å bruke disse teknikkene kan React-applikasjoner bli mer effektive og tilby en jevnere brukeropplevelse. Det er også viktig å være bevisst på at ved å bruke inline event handlers kan vi noen ganger forenkle koden ved å unngå å opprette ekstra funksjoner, men dette kan også føre til utfordringer knyttet til ytelse hvis det ikke håndteres riktig.

Endtext

Hvordan teste funksjoner med bivirkninger og avhengigheter i enhetstesting

En viktig del av enhetstesting er å teste funksjoner som har bivirkninger, eller som er avhengige av eksterne faktorer som systemtid, API-kall eller biblioteker. Slike funksjoner kan være utfordrende å teste på grunn av deres sideeffekter, som kan gjøre testene ustabile eller avhengige av miljøet de kjører i. Dette betyr imidlertid ikke at de ikke kan testes. Det finnes metoder som gjør det mulig å håndtere disse utfordringene, og en av de viktigste teknikkene er mocking.

Mocking innebærer å erstatte eller emulere eksternt avhengige funksjoner eller moduler med falske implementasjoner som returnerer forhåndsdefinerte verdier. Dette lar oss isolere funksjonens logikk og teste den uten å være avhengig av eksterne faktorer.

Et eksempel på dette kan være testing av funksjoner som bruker systemets tid, for eksempel en funksjon som utfører en handling etter et visst tidsintervall. I stedet for å vente i sanntid, kan vi manipulere tidens fremdrift i testene ved å bruke metoder som kontrollerer tidtakere og intervaller.

Et vanlig scenario som kan kreve mocking, er testing av funksjoner som involverer tidtakere eller gjentatte hendelser, som i følgende eksempel:

js
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 eksemplet håndterer vi tidsbaserte funksjoner uten å vente på ekte tid ved å bruke vi.useFakeTimers() for å simulere tidens gang. Funksjonen vi.runAllTimers() tvinger testen til å kjøre alle ventende tidtakere umiddelbart, mens vi.advanceTimersByTime() lar oss hoppe fremover i tid. Dette gir oss muligheten til å teste tidsrelaterte funksjoner raskt og presist uten å måtte vente.

Mocking kan også være nyttig når man tester funksjoner som interagerer med eksterne API-er eller systemer, som for eksempel en funksjon som henter data fra en server. I stedet for å gjøre ekte nettverkskall under testene, kan vi simulere nettverksresponsen. Et eksempel på dette kan være testing av en funksjon som bruker systemets helsedata:

js
import { 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(); }); });

I dette tilfellet bruker vi vi.mock til å erstatte den originale implementeringen av getSteps med en mock som returnerer et forhåndsdefinert antall steg. På denne måten kan vi teste funksjonen uten å være avhengig av den faktiske iOS Health Kit API-en, noe som kan være vanskelig å simulere under testene.

Testing av slike funksjoner med bivirkninger og eksterne avhengigheter krever at vi forstår hvordan vi kan kontrollere miljøet der testene kjører. Mocking gir oss full kontroll over denne prosessen, og gjør det mulig å simulere forskjellige scenarioer som kan være vanskelig å replikere i en ekte testsituasjon. Det er en essensiell teknikk for å sikre pålitelige og effektive enhetstester, spesielt når funksjonene vi tester har uforutsigbare eller tid-sensitive aspekter.

Det er viktig å merke seg at mocking ikke bare handler om å erstatte funksjoner som henter data fra eksterne kilder, men også om å kontrollere hvordan tid og eksterne systemer interagerer med koden vår. For eksempel kan mocking av systemtid være nødvendig for å teste funksjoner som er avhengige av datoer og tidssoner. Videre kan det være nyttig å bruke mocking for å isolere testene fra bivirkninger som kan oppstå ved å kommunisere med eksterne servere eller systemer som ikke er tilgjengelige under testen.

Mocking er derfor en viktig strategi for å forbedre påliteligheten til enhetstester, men det krever nøye vurdering og planlegging for å sikre at testene fortsatt reflekterer realistiske og meningsfulle scenarioer.