---
title: "`requestAnimationFrame` loop in an effect without `cancelAnimationFrame` (lint/raf-without-cancel)"
description: "lint/raf-without-cancel: The loop keeps running after unmount (and duplicates on remount in Strict Mode), burning CPU/battery and calling setState on…"
canonical: https://void-design.vercel.app/rules/lint/raf-without-cancel
lastModified: 2026-09-16
---

# `requestAnimationFrame` loop in an effect without `cancelAnimationFrame`

`lint/raf-without-cancel` · severity **warn** · category Lint · detected by `void lint` and `void audit`

## 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

```tsx
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-render` Browser global (`window`, `document`, `navigator`, `localStorage`) at module scope or during render — [lint/browser-global-in-render](https://void-design.vercel.app/rules/lint/browser-global-in-render)
- `timer-in-render` `setTimeout` / `setInterval` called in a component body — [lint/timer-in-render](https://void-design.vercel.app/rules/lint/timer-in-render)
- `motionvalue-subscribe-in-render` MotionValue `.on("change")` subscription in a component body — [lint/motionvalue-subscribe-in-render](https://void-design.vercel.app/rules/lint/motionvalue-subscribe-in-render)
- `webgl-in-map` WebGL canvas rendered inside `.map()` — [lint/webgl-in-map](https://void-design.vercel.app/rules/lint/webgl-in-map)
- `listener-without-cleanup` Event listener or interval added in an effect without cleanup — [lint/listener-without-cleanup](https://void-design.vercel.app/rules/lint/listener-without-cleanup)
- `scroll-listener-nonpassive` `wheel` / `touchstart` / `touchmove` listener without `{ passive: true }` — [lint/scroll-listener-nonpassive](https://void-design.vercel.app/rules/lint/scroll-listener-nonpassive)
- `unload-listener` `unload` event listener — [lint/unload-listener](https://void-design.vercel.app/rules/lint/unload-listener)
- `useeffect-fetch` Page fetches its data in `useEffect` — [lint/useeffect-fetch](https://void-design.vercel.app/rules/lint/useeffect-fetch)

Detected by `void lint` and `void audit`. Explain it in a terminal: `void rules lint/raf-without-cancel`
