Understanding useRef Hook in React: A Simple Countdown Timer Example

Understanding useRef Hook in React: A Simple Countdown Timer Example

ยท

4 min read

Today, during an interview session, I encountered an interesting scenario. I asked the candidate to create a simple countdown timer using React. However, despite his knowledge of React basics, he struggled to implement the timer functionality correctly.

This incident highlighted the importance of understanding useRef, a powerful hook provided by React for managing mutable values that persist across renders. In this blog post, I'll delve into the useRef hook and demonstrate its usage with the help of the given countdown timer example.

What is useRef?

The useRef hook in React allows us to create mutable references that persist between renders. It returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The value of .current can be changed without triggering a re-render, making it ideal for storing mutable values that need to persist across renders.

Why useRef?

  1. Preservation of Values: useRef is particularly useful for storing values that need to persist between renders without triggering re-renders. This makes it suitable for storing references to DOM elements, timers, and other mutable data.

  2. Avoiding Stale Closures: When dealing with closures in event handlers or asynchronous code, useRef can help avoid issues caused by stale closures by providing a stable reference to mutable values.

  3. Efficient DOM Manipulation: useRef can be used to directly access and manipulate DOM elements, bypassing the need for repetitive querying and updating of the DOM through React's virtual DOM.

Countdown Timer Example

Let's revisit the countdown timer example that caused confusion during the interview. We want to create a simple countdown timer where users can input minutes and seconds, start, stop, and reset the timer.

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function App() {
  const [min, setMin] = useState();
  const [sec, setSec] = useState();
  const [Originalmin, setOriginalMin] = useState();
  const [originalsec, setOriginalSec] = useState();
  const [start, setStart] = useState(true);
  let timer = useRef(null);

  const startTimer = () => {
    timer.current = setInterval(function () {
      setSec((per) => per - 1);
    }, 1000);
  };

  const handleStartStop = () => {
    if (sec && min) {
      setStart(!start);
      if (start) {
        startTimer();
      } else {
        clearInterval(timer.current);
      }
    } else {
      alert("Pleas fill the Minute and Seconds to start the timer");
    }
  };

  const handleReset = () => {
    setStart(true);
    clearInterval(timer.current);
    setMin(Originalmin);
    setSec(originalsec);
  };

  useEffect(() => {
    // console.log("MIN", min);
    // console.log("Sec", sec);
    // console.log(timer);
    if (sec == 0) {
      setSec(59);
      if (min <= 1) {
        setMin((min) => min - 1);
      }
    }
    if (sec == 0 && min == 0) {
      clearInterval(timer.current);
    }
  }, [min, sec]);
  return (
    <div className="App">
      <input
        type="number"
        value={min}
        onChange={(e) => {
          setMin(e.target.value);
          setOriginalMin(e.target.value);
        }}
        placeholder="Minute"
      />

      <input
        type="number"
        value={sec}
        onChange={(e) => {
          setSec(e.target.value);
          setOriginalSec(e.target.value);
        }}
        placeholder="Second"
      />

      <button onClick={handleStartStop}>{start ? "Start" : "Stop"}</button>
      <button onClick={handleReset}>Reset</button>
      <div className="timer">
        <p>{min} MM </p>
        <p>{sec} SEC</p>
      </div>
    </div>
  );
}

How useRef is Utilized

In the above code, we utilize the useRef hook to store the timer ID (timer.current) for the setInterval function. This ensures that the timer ID persists across re-renders and allows us to clear the interval when needed without losing reference to it.

const timer = useRef(null);

We initialize the useRef hook with null as the initial value. Later, we assign the timer ID returned by setInterval to timer.current within the startTimer function.

const startTimer = () => {
  timer.current = setInterval(function () {
    setSec((per) => per - 1);
  }, 1000);
};

Similarly, we use timer.current to clearInterval when the timer is stopped or reset.

const handleStartStop = () => {
    if (sec && min) {
      setStart(!start);
      if (start) {
        startTimer();
      } else {
        clearInterval(timer.current);
      }
    } else {
      alert("Pleas fill the Minute and Seconds to start the timer");
    }
  };

  const handleReset = () => {
    setStart(!start);
    clearInterval(timer.current);
    setMin(Originalmin);
    setSec(originalsec);
  };

Conclusion

Understanding useRef and its role in managing mutable values across renders is essential for building robust React applications. By correctly utilizing useRef, we can avoid common pitfalls such as losing reference to mutable values and ensure smoother interactions within our components.

I hope this explanation clarifies the concept of useRef and its practical application in React development.

<HappyCoding/>

CodeSandBox link: https://codesandbox.io/p/sandbox/timer-x2mr6m

If you like the blog please do follow and like the blog.๐Ÿ˜

ย