All together now! Below I am going to explain a custom hook created to use setInterval to aid in the creation of a very basic countdown clock.

Full disclosure, when I initially choose to do a countdown clock using setInterval I found it somewhat difficult to work with. After some searching, the final pieces of this custom hook useInterval were gleaned from Dan Abramov's blog post specifically on using setInterval in hooks and I encourage you to check it out for a more in-depth discussion of why setInterval is particularly tricky to make declarative with React hooks.

If you would like to go straight to the working code, you can see it here on Code Sandbox.

Here is the actual custom hook useInterval created for this:

import React, { useEffect, useState, useRef } from "react"; function useInterval(callback, delay) { const savedCallback = useRef(); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); }

Countdown component that makes use of the useInterval custom hook along with useRef to keep the same reference to the initial date and useState to hold both the countdownDuration and the isRunning state that the button uses to pause and un-pause the countdown.

Using the custom useInterval hook

function Countdown() { const initDateTime = useRef(Date.now()); const [countdownDuration, setCountdownDuration] = useState( initDateTime.current + 123456789 // Random number choosen to have as a timer duration ); const [isRunning, setIsRunning] = useState(true); useInterval( () => { setCountdownDuration(countdownDuration - 1000); }, isRunning ? 1000 : null ); useInterval(() => { if (countdownDuration < 1000) { setIsRunning(false); // stop running countdown on the last second } }, 1000); function handleChangeCountdown() { setIsRunning(!isRunning); // Reverse the state of isRunning on each button click } return ( <div> <CountdownClock countdownDuration={countdownDuration} initDateTime={initDateTime.current} /> {countdownDuration < 1000 && <h2>Countdown Reached!</h2>} <button onClick={handleChangeCountdown}> {isRunning ? "Pause Countdown" : "Continue Countdown"} </button> </div> ); }

Since we don't need to update the state of the initial date, and we would like its value to remain consistent through each render, a useRef is appropriate here as it functions as an instance variable in a class.

Here is the full Countdown.js file if you didn't want to click over to the Code Sandbox Example:

Countdown.js

import React, { useEffect, useState, useRef } from "react"; import CountdownClock from "./CountdownClock"; function useInterval(callback, delay) { const savedCallback = useRef(); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); } function Countdown() { const initDateTime = useRef(Date.now()); // since we don't need to change this value, we can use a ref const [countdownDuration, setCountdownDuration] = useState( initDateTime.current + 123456789 // Random number choosen for this ); const [isRunning, setIsRunning] = useState(true); useInterval( () => { setCountdownDuration(countdownDuration - 1000); }, isRunning ? 1000 : null ); useInterval(() => { if (countdownDuration < 1000) { setIsRunning(false); } }, 1000); function handleChangeCountdown() { setIsRunning(!isRunning); } return ( <div> <h1>Learning React Hooks</h1> <CountdownClock countdownDuration={countdownDuration} initDateTime={initDateTime.current} /> {countdownDuration < 1000 && <h2>Countdown Reached!</h2>} <button onClick={handleChangeCountdown}> {isRunning ? "Pause Countdown" : "Continue Countdown"} </button> </div> ); } export default Countdown;

If you are interested in how the CountdownClock component is defined, here it is below, but it's not nearly as important to the subject matter of hooks as the previous two code blocks are on the definition of the useInterval custom hook and the use of it in the Countdown component.

CountdownClock.js

import React from "react"; import moment from "moment"; function getPluralOutput(duration, name) { return duration > 0 && duration < 2 ? `${duration} ${name}` : `${duration} ${name}s`; } function CountdownClock({ countdownDuration, initDateTime }) { const initMoment = moment(initDateTime); const currentDuration = moment(countdownDuration); const duration = moment.duration(currentDuration.diff(initMoment)); // Get initial values let w = duration.weeks(); let d = duration.days(); let h = duration.hours(); let m = duration.minutes(); let s = duration.seconds(); // Set display values w = getPluralOutput(w, "week"); d = getPluralOutput(d, "day"); h = getPluralOutput(h, "hour"); m = getPluralOutput(m, "minute"); s = getPluralOutput(s, "second"); return <h3>{`${w} ${d} ${h} ${m} ${s}`}</h3>; } export default CountdownClock;

Resources