3 · Reliability and delivery · 45 MIN
Project · A timer with reliable cleanup
An effect owns an external subscription’s lifetime.
setInterval creates work outside React. The effect starts that work and its cleanup clears it when the component unmounts or the running dependency changes. A functional state updater avoids capturing an old seconds value. The parent can hide the timer to test cleanup. React development Strict Mode runs an extra setup-and-cleanup cycle to expose missing cleanup, so a correct effect must tolerate restarting. Browser timers are delayed by background throttling; this is a practice counter, not a precise elapsed-time clock.
Verify no duplicate ticking under development Strict Mode.
Read the example
import {useEffect,useState} from 'react';
function Timer() {
const [seconds,setSeconds]=useState(0),[running,setRunning]=useState(true);
useEffect(()=>{if(!running)return;const id=setInterval(()=>setSeconds(n=>n+1),1000);return()=>clearInterval(id)},[running]);
return <section><h2>Practice timer</h2><p>{seconds} ticks</p><button onClick={()=>setRunning(v=>!v)}>{running?'Pause':'Resume'}</button></section>;
}
export default function App(){const [show,setShow]=useState(true);return <main><h1>Study session</h1><button onClick={()=>setShow(v=>!v)}>Toggle timer</button>{show&&<Timer/>}</main>}
Check the expected output
The counter advances while mounted and running. Pause stops increments. Hiding and showing creates a new counter starting at zero.
Your challenge
Build a study-session view combining this timer, task completion and searchable course choices; document which state survives unmounting.
Solution cost: O(1) per timer tick; wall-clock precision is not guaranteed. time · One active interval per mounted running timer. space
Common trap
An interval without cleanup keeps doing work after its owning component disappears.
Study the project implementation
import {useEffect,useState} from 'react';
function Timer() {
const [seconds,setSeconds]=useState(0),[running,setRunning]=useState(true);
useEffect(()=>{if(!running)return;const id=setInterval(()=>setSeconds(n=>n+1),1000);return()=>clearInterval(id)},[running]);
return <section><h2>Practice timer</h2><p>{seconds} ticks</p><button onClick={()=>setRunning(v=>!v)}>{running?'Pause':'Resume'}</button></section>;
}
export default function App(){const [show,setShow]=useState(true);return <main><h1>Study session</h1><button onClick={()=>setShow(v=>!v)}>Toggle timer</button>{show&&<Timer/>}</main>}
Further reading: Official documentation