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;
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.