Hoppa till huvudinnehåll

Custom hook: useInterval

· 4 min att läsa
Filip Tammergård
Programmerare på Frilans Finans

setInterval kör en callback i ett bestämt intervall. Att använda funktionen i React innebär att man måste komma ihåg att anropa clearInterval vid unmount och dra in extra state varje gång intervallet ska pausas eller startas om. Det här inlägget paketerar båda delarna i en liten custom hook som jag kallar useInterval.

TL;DR

  • useEffect startar intervallet och returnerar en cleanup som anropar clearInterval – ingen manuell unmount-hantering.
  • Den senaste callbacken hålls i en ref, så intervallet fortsätter ticka i jämn takt även när komponenten renderar om av orelaterade skäl.
  • Att skicka in null som delay pausar intervallet, så det går att härleda "körs nu" direkt från props eller state utan en separat flagga.
  • När delay ändras startas intervallet om automatiskt, så ett dynamiskt värde "bara funkar".

Testa att ändra delay nedan – under 50 ms pausas räknaren, och när värdet höjs igen fortsätter den där den var:

0

Hooken

import { useEffect, useRef } from "react"

export const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef(callback)

useEffect(() => {
savedCallback.current = callback
}, [callback])

useEffect(() => {
if (delay === null) return
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}

Det är två effekter. Den första ser till att savedCallback pekar på den senaste callback vid varje rendering. Den andra äger timern: så länge delay är ett tal är ett intervall igång, och när det blir null hoppar effekten över setupen och cleanupen från förra renderingen river ner timern.

Att dela upp det så här spelar roll eftersom intervallet bara beror på delay. En naiv variant med [callback, delay] skulle riva ner och återskapa timern vid varje rendering, eftersom callbacken oftast är en ny arrow-funktion varje gång – så en komponent som renderar om snabbare än sitt delay (säg ett fält som triggar vid varje tangenttryck) skulle kunna nollställa timern om och om igen och svälta ut den. Att läsa callbacken via en ref i stället innebär att intervallet skapas en gång per delay och alltid anropar den senaste versionen. Det här är mönstret från Dan Abramovs "Making setInterval Declarative with React Hooks". Ett dynamiskt delay behöver fortfarande ingen specialhantering – när det ändras körs cleanupen och ett nytt intervall startar.

Att använda hooken

Det enklaste anropet har samma API som setInterval, fast utan cleanup-boilerplate:

useInterval(() => {
console.log("Där gick en sekund!")
}, 1000)

Hooken kommer verkligen till sin rätt när delay ska kunna ändras under körning, eller när intervallet ska pausas utifrån något annat state. Här är hela komponenten bakom räknaren ovan:

import { useInterval } from "hooks/useInterval"
import { type ChangeEvent, useState } from "react"

function Counter() {
const [delay, setDelay] = useState(500)
const [count, setCount] = useState(0)

useInterval(() => setCount((prev) => prev + 1), delay >= 50 ? delay : null)

function handleDelayChange(e: ChangeEvent<HTMLInputElement>) {
setDelay(parseInt(e.target.value, 10))
}

return (
<div>
<label>
Välj delay [ms]
<br />
<input type="number" onChange={handleDelayChange} value={delay} />
</label>
<p>{count}</p>
</div>
)
}

Att villkorligt skicka in null är det som får pausen att fungera – useInterval ser att delay ändras, river ner den gamla timern, och startar ingen ny förrän värdet är ett tal igen. Ingen isRunning-flagga, ingen extra useEffect, bara ett enda uttryck.

Var den används på bloggen

useInterval driver flera andra inlägg här:

Referenser