Why it matters
The loop keeps running after unmount (and duplicates on remount in Strict Mode), burning CPU/battery and calling setState on unmounted components.
How to fix it
Store the frame id and cancel it in the effect cleanup. Also pause off-screen loops via IntersectionObserver / visibilitychange.
Example
useEffect(() => {
let id = 0
const loop = (t: number) => { draw(t); id = requestAnimationFrame(loop) }
id = requestAnimationFrame(loop)
return () => cancelAnimationFrame(id)
}, [])More lint rules
void lint reports 67 rules in this category. Static source checks with no browser: Tailwind v4 silent failures, Next.js 16 API traps, React render-body bugs, accessibility markup, SEO files and motion hygiene.
browser-global-in-renderBrowser global (`window`, `document`, `navigator`, `localStorage`) at module scope or during rendertimer-in-render`setTimeout` / `setInterval` called in a component bodymotionvalue-subscribe-in-renderMotionValue `.on("change")` subscription in a component bodywebgl-in-mapWebGL canvas rendered inside `.map()`listener-without-cleanupEvent listener or interval added in an effect without cleanupscroll-listener-nonpassive`wheel` / `touchstart` / `touchmove` listener without `{ passive: true }`unload-listener`unload` event listeneruseeffect-fetchPage fetches its data in `useEffect`