three-prefer-set-animation-loop reports a 2D canvas requestAnimationFrame loop whenever the package depends on three, even when the file never imports Three.js and never constructs a renderer.
Applying the recommended renderer.setAnimationLoop(...) fix is invalid here: CanvasRenderingContext2D has no such API, so the “fix” would throw at runtime.
Reproduction
Reproduced with React Doctor 0.9.13, on macOS. JSON report: schemaVersion: 3, ok: true, project complete: true, hasThree: true. This is a static-analysis reproduction; no runtime WebXR or WebGL failure is inferred from the warning.
Create a directory with this package.json, add the file below, and run:
npx --yes react-doctor@0.9.13 . --scope full --json --blocking none --yes --no-score
{
"name": "react-doctor-raf-fp",
"private": true,
"version": "1.0.0",
"dependencies": {
"react": "19.2.4",
"three": "0.185.1"
}
}
dither.tsx:
import { useEffect, useRef } from "react";
export function DitherBackground() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const context = canvas.getContext("2d");
if (!context) {
return;
}
let animationFrame = 0;
const drawFrame = () => {
context.clearRect(0, 0, 1, 1);
context.fillRect(0, 0, 1, 1);
};
const animate = () => {
drawFrame();
animationFrame = requestAnimationFrame(animate);
};
animationFrame = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationFrame);
}, []);
return <canvas ref={canvasRef} />;
}
Actual
“This continuous Three.js animation loop is driven by requestAnimationFrame. Use renderer.setAnimationLoop(callback) for renderer-managed timing and compatibility”
The file has no three import, no WebGLRenderer / WebGPURenderer, and no WebXR session. requestAnimationFrame is the correct clock for a 2D canvas.
Expected
Do not report a recursive requestAnimationFrame loop unless the same file (or a proven Three.js renderer in scope) is actually driving a Three.js renderer.
The rule is project-gated on the three capability and then matches any global unconditional requestAnimationFrame self-schedule. That is enough to flag every 2D canvas / DOM animation loop in an app that also has an unrelated R3F scene.
Suggested regression (should be length 0):
it("allows a 2D canvas animation loop that never constructs a Three.js renderer", () => {
const code = `
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
function frame() {
context.fillRect(0, 0, 1, 1);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
`;
expect(runRule(threePreferSetAnimationLoop, code).diagnostics).toHaveLength(0);
});
Notes
Rule source: packages/oxlint-plugin-react-doctor/src/plugin/rules/r3f/three-prefer-set-animation-loop.ts reports every unconditional recursive requestAnimationFrame with no renderer check.
The existing test “reports a recursive loop that delegates rendering to an imported viewer” encodes that breadth. Tests that construct WebGLRenderer are the intended hits; a same-file getContext("2d") loop should not inherit them just because package.json lists three.
three-prefer-set-animation-loopreports a 2D canvasrequestAnimationFrameloop whenever the package depends onthree, even when the file never imports Three.js and never constructs a renderer.Applying the recommended
renderer.setAnimationLoop(...)fix is invalid here:CanvasRenderingContext2Dhas no such API, so the “fix” would throw at runtime.Reproduction
Reproduced with React Doctor 0.9.13, on macOS. JSON report:
schemaVersion: 3,ok: true, projectcomplete: true,hasThree: true. This is a static-analysis reproduction; no runtime WebXR or WebGL failure is inferred from the warning.Create a directory with this
package.json, add the file below, and run:npx --yes react-doctor@0.9.13 . --scope full --json --blocking none --yes --no-score{ "name": "react-doctor-raf-fp", "private": true, "version": "1.0.0", "dependencies": { "react": "19.2.4", "three": "0.185.1" } }dither.tsx:Actual
“This continuous Three.js animation loop is driven by requestAnimationFrame. Use renderer.setAnimationLoop(callback) for renderer-managed timing and compatibility”
The file has no
threeimport, noWebGLRenderer/WebGPURenderer, and no WebXR session.requestAnimationFrameis the correct clock for a 2D canvas.Expected
Do not report a recursive
requestAnimationFrameloop unless the same file (or a proven Three.js renderer in scope) is actually driving a Three.js renderer.The rule is project-gated on the
threecapability and then matches any global unconditionalrequestAnimationFrameself-schedule. That is enough to flag every 2D canvas / DOM animation loop in an app that also has an unrelated R3F scene.Suggested regression (should be length 0):
Notes
Rule source:
packages/oxlint-plugin-react-doctor/src/plugin/rules/r3f/three-prefer-set-animation-loop.tsreports every unconditional recursiverequestAnimationFramewith no renderer check.The existing test “reports a recursive loop that delegates rendering to an imported viewer” encodes that breadth. Tests that construct
WebGLRendererare the intended hits; a same-filegetContext("2d")loop should not inherit them just becausepackage.jsonliststhree.