DeployU
Interviews / Frontend Engineering / This component has issues managing a timer ID. Fix it with `useRef`.

Questions

This component has issues managing a timer ID. Fix it with `useRef`.

practical Hooks & Patterns Interactive Quiz Code Examples

A developer is building a Timer component that needs to set up a setInterval when it mounts and clear it when it unmounts. They are trying to store the intervalId (returned by setInterval) in a useState variable so they can access it in the cleanup function.

However, they’ve noticed that every time the component re-renders (e.g., due to a parent component’s state change), the intervalId state update causes another re-render, leading to unexpected behavior or even an infinite loop if not handled carefully.

The Challenge

You’ve been given the Timer component.

  1. Explain why storing the intervalId in useState is problematic in this scenario.
  2. Fix the component to correctly store the intervalId using the useRef hook, ensuring it persists across renders without triggering unnecessary re-renders.
Your Code
import React, { useState, useEffect } from 'react';

export default function Timer() {
  const [seconds, setSeconds] = useState(0);
  // THE BUG: Storing intervalId in useState.
  // Updating this state will cause re-renders, which is not needed for a mutable ID.
  const [intervalId, setIntervalId] = useState(null);

  useEffect(() => {
    console.log('Setting up interval...');
    const id = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    // Storing the ID in state causes a re-render, which can be problematic.
    setIntervalId(id);

    return () => {
      console.log('Clearing interval...');
      clearInterval(intervalId); // This intervalId might be stale if not careful
    };
  }, []); // Empty dependency array to run once on mount/unmount

  return (
    <div style={{ padding: '20px' }}>
      <h1>Timer: {seconds}s</h1>
      <p>Check the console for setup/cleanup logs.</p>
    </div>
  );
}

function App() {
  const [showTimer, setShowTimer] = useState(true);
  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>
        {showTimer ? 'Hide Timer' : 'Show Timer'}
      </button>
      <hr />
      {showTimer && <Timer />}
    </div>
  );
}
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Solution

Wrong Approach

export default function Timer() { const [seconds, setSeconds] = useState(0); // THE BUG: Storing intervalId in useState. // Updating this state will cause re-renders, which is not needed for a mutable ID. const [intervalId, setIntervalId] = useState(null); useEffect(() => { console.log('Setting up interval...'); const id = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); }, 1000); // Storing the ID in state causes a re-render, which can be problematic. setIntervalId(id); return () => { console.log('Clearing interval...'); clearInterval(intervalId); // This intervalId might be stale if not careful }; }, []); // Empty dependency array to run once on mount/unmount return ( <div style={{ padding: '20px' }}> <h1>Timer: {seconds}s</h1> <p>Check the console for setup/cleanup logs.</p> </div> ); }

Right Approach

export default function Timer() { const [seconds, setSeconds] = useState(0); // FIX: Use useRef to store the intervalId. // Updating intervalRef.current does NOT trigger a re-render. const intervalRef = useRef(null); useEffect(() => { console.log('Setting up interval...'); const id = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); }, 1000); // Store the interval ID in the ref's .current property. intervalRef.current = id; return () => { console.log('Clearing interval...'); // Access the stored ID from the ref's .current property for cleanup. clearInterval(intervalRef.current); }; }, []); // Empty dependency array to run once on mount/unmount return ( <div style={{ padding: '20px' }}> <h1>Timer: {seconds}s</h1> <p>Check the console for setup/cleanup logs.</p> </div> ); }

Why This Works

The problem arises from using useState to store the intervalId. While useState is perfect for managing state that should trigger re-renders, it’s not ideal for values that are mutable but whose changes should not cause a re-render.

When setIntervalId(id) is called inside useEffect, it updates the component’s state. This state update triggers a re-render of the Timer component. If the useEffect’s dependency array is not carefully managed, this can lead to an infinite loop or multiple intervals running simultaneously (as seen in the Strict Mode example). Even with an empty dependency array, the setIntervalId call itself causes a re-render, which is unnecessary for a value that’s only used internally for cleanup.

The Fix: Using useRef for Mutable, Non-Rendering Values

The useRef hook is designed for exactly this kind of scenario. It returns a mutable ref object whose .current property can hold any value. Crucially, updating the .current property of a ref does not trigger a re-render of the component. The ref object itself persists across renders.

This makes useRef ideal for:

  • Accessing DOM elements directly.
  • Storing mutable values (like timer IDs, previous state values, or any instance variable) that need to persist across renders but whose changes shouldn’t cause the component to update its UI.

By using useRef, the intervalId is stored in a mutable container that persists across renders without causing any additional re-renders when it’s updated. This ensures the setInterval is managed correctly and efficiently.

Practice Question

When should you use the useRef hook instead of useState?