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>
setIntervalalready has a delay at the first time, do you need a different delay timeout than the interval period?