getting started
Consider detangling the concerns of your component and writing small pieces. Here we have a useInterval
custom hook which strictly defines the setInterval
portion of the program. I added some console.log
lines so we can observe the effects -
// rough draft
// read on to make sure we get all the parts right
function useInterval (f, delay)
{ const [timer, setTimer] =
useState(null)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
Now when we write MyComp
we can handle the setTimeout
portion of the program -
function MyComp (props)
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000) // first try
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
}
Now we can useInterval
in various parts of our program, and each one can be used differently. All the logic for the start, stop and cleanup is nicely encapsulated in the hook.
Here's a demo you can run to see it working -
const { useState, useEffect } = React
const useInterval = (f, delay) =>
{ const [timer, setTimer] =
useState(undefined)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
const MyComp = props =>
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000)
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
};
ReactDOM.render
( <MyComp/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting it right
We want to make sure our useInterval
hook doesn't leave any timed functions running if our timer are stopped or after our components are removed. Let's test them out in a more rigorous example where we can add/remove many timers and start/stop them at any time -

A few fundamental changes were necessary to make to useInterval
-
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
Using useInterval
in MyTimer
component is intuitive. MyTimer
is not required to do any sort of cleanup of the interval. Cleanup is automatically handled by useInterval
-
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now()) // <-- for demo
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...") // <-- for demo
setTimeout(() => {
console.log("starting...") // <-- for demo
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button onClick={start} disabled={busy} children="Start" />
<button onClick={stop} disabled={!busy} children="Stop" />
</span>
}
The Main
component doesn't do anything special. It just manages an array state of MyTimer
components. No timer-specific code or clean up is required -
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
Expand the snippet below to see useInterval
working in your own browser. Fullscreen mode is recommended for this demo -
const { useState, useEffect } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now())
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...")
setTimeout(() => {
console.log("starting...")
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting advanced
Let's imagine an even more complex useInterval
scenario where the timed function, f
, and the delay
can change -

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = // ...
const interval = useRef(f)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
// ...
const t =
setInterval(_ => interval.current(), delay)
// stop
// ...
}, [busy, delay])
return // ...
}
Now we can edit MyTimer
to add the doubler
and turbo
state -
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] = useState(0)
const [doubler, setDoubler] = useState(false) // <--
const [turbo, setTurbo] = useState(false) // <--
const [start, stop, busy] =
useInterval
( doubler // <-- doubler changes which f is run
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo // <-- turbo changes delay
? Math.floor(delay / 2)
: delay
)
// ...
Then we add a double and turbo button -
// ...
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
{/* start button ... */}
<button
onClick={toggleDoubler} // <--
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo} // <--
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
{/* stop button ... */}
</span>
}
Expand the snippet below to run the advanced timer demo in your own browser -
const { useState, useEffect, useRef, useCallback } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const interval = useRef(f)
const [busy, setBusy] = useState(0)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t =
setInterval(_ => interval.current(), delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, ... props })
{ const [counter, setCounter] =
useState(0)
const [doubler, setDoubler] = useState(false)
const [turbo, setTurbo] = useState(false)
const [start, stop, busy] =
useInterval
( doubler
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo
? Math.floor(delay / 2)
: delay
)
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={toggleDoubler}
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo}
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>