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.