DEV Community

Manav Bajaj
Manav Bajaj

Posted on

🚀 Mastering React’s useState and useRef: A Stopwatch Example

React’s useState and useRef hooks are a dynamic duo for building interactive UIs. useState manages reactive state, triggering re-renders to update the UI when values change. useRef, on the other hand, holds mutable values that persist across renders without causing re-renders—perfect for storing data like timers or DOM references. Together, they enable clean, efficient solutions for features like counters or timers.

Below, I’ve shared a stopwatch app that combines useState and useRef to track elapsed time with start, stop, and reset functionality. Let’s break down how it works!

import { useState, useRef } from 'react';
import './App.css';

function App() {
  // State to store the start time of the current timer session
  const [startTime, setStartTime] = useState(null);
  // State to store the current time, updated every 10ms for real-time display
  const [now, setNow] = useState(null);

  // Ref to store the interval ID for cleanup
  const intervalRef = useRef(null);
  // Ref to store cumulative elapsed time across start/stop cycles
  const leapTimeRef = useRef(0);

  // Starts the stopwatch
  const handleStart = () => {
    const currentTime = Date.now(); // Get current timestamp
    setStartTime(currentTime); // Set start time
    setNow(currentTime); // Initialize current time for display
    clearInterval(intervalRef.current); // Clear any existing interval
    // Start interval to update 'now' every 10ms
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  };

  // Stops the stopwatch and accumulates elapsed time
  const handleStop = () => {
    clearInterval(intervalRef.current); // Stop the interval
    if (startTime != null && now != null) {
      // Add elapsed time of current session to leapTimeRef
      leapTimeRef.current += (now - startTime) / 1000;
    }
    setStartTime(null); // Reset startTime to prevent miscalculations
  };

  // Resets the stopwatch to initial state
  const handleReset = () => {
    clearInterval(intervalRef.current); // Stop the interval
    leapTimeRef.current = 0; // Reset cumulative time
    setStartTime(null); // Clear start time
    setNow(null); // Clear current time
  };

  // Calculate total elapsed time (persistent + current session)
  let secondsPassed = leapTimeRef.current;
  if (startTime != null && now != null) {
    secondsPassed += (now - startTime) / 1000; // Add current session time
  }

  // Render the stopwatch UI
  return (
    <div className="App">
      <p>{secondsPassed.toFixed(2)}</p> {/* Display time with 2 decimal places */}
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

How It Works:

1) State Management with useState:

  • startTime stores the timestamp when the stopwatch starts (set via setStartTime).
  • now updates every 10ms with the current timestamp (via setNow), triggering re-renders to display real-time elapsed time.
  • These reactive states ensure the UI reflects the timer’s current state.

2) Persistent Data with useRef:

  • intervalRef holds the ID of the setInterval timer, allowing us to clear it when stopping or resetting without affecting renders.
  • leapTimeRef tracks the cumulative elapsed time (in seconds) across multiple start/stop cycles, persisting without triggering re-renders.

3) Function Breakdown:

  • handleStart: Sets startTime and now to the current timestamp, clears any existing interval, and starts a new interval to update now every 10ms.
  • handleStop: Stops the interval, calculates the elapsed time since startTime, adds it to leapTimeRef.current, and resets startTime to prevent miscalculations.
  • handleReset: Clears the interval, resets leapTimeRef to 0, and clears startTime and now for a fresh start.

4) Displaying Time:

  • secondsPassed combines leapTimeRef.current (past time) with the current session’s time (now - startTime) if running.
  • Displayed with .toFixed(2) for two decimal places (e.g., 12.34 seconds).

Why This Combo Shines:

  • useState drives real-time UI updates for a responsive experience.
  • useRef ensures leapTimeRef and intervalRef persist efficiently, avoiding unnecessary re-renders.
  • Together, they create a smooth, accurate stopwatch.

đź’ˇ Pro Tip:
Use useState for UI-driving data and useRef for persistent, non-rendering data. This pattern is key for performant React apps!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.