no-leaked-timeout
Enforces that every 'setTimeout' in a component or custom hook has a corresponding 'clearTimeout'.
Full Name in eslint-plugin-react-web-api
react-web-api/no-leaked-timeoutFull Name in @eslint-react/eslint-plugin
@eslint-react/web-api-no-leaked-timeoutPresets
web-api
recommended
recommended-typescript
recommended-type-checked
strict
strict-typescript
strict-type-checked
Rule Details
Scheduling a timeout within the setup function of useEffect without canceling it in the cleanup function can lead to unwanted setTimeout callback executions and may also result in stale values captured by previous renders' effects after each subsequent re-render.
Examples
Setting timeout without cleanup in useEffect
import React, { useEffect, useState } from "react";
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// Problem: A 'setTimeout' created in 'useEffect' must be cleared in the cleanup function.
const timeoutId = setTimeout(() => console.log(count), 1000);
}, []);
return null;
}Clearing timeout in useEffect cleanup
import React, { useEffect, useState } from "react";
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// Recommended: Store the timeout ID
const timeoutId = setTimeout(() => console.log(count), 1000);
// Recommended: Clear the timeout in the cleanup function
return () => clearTimeout(timeoutId);
}, []);
return null;
}Canceling a timeout when dependencies change
When a timeout depends on reactive values like props or state, clear the previous timeout in the cleanup function before scheduling a new one. This prevents outdated timeouts from firing after the value has already changed.
// Recommended: clear the previous timeout before scheduling a new one
import { useState, useEffect } from "react";
function Playground() {
const [text, setText] = useState("a");
useEffect(() => {
const timeoutId = setTimeout(() => {
console.log("⏰ " + text);
}, 3000);
return () => {
clearTimeout(timeoutId);
};
}, [text]);
return (
<>
<label>
What to log:{" "}
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
</label>
<h1>{text}</h1>
</>
);
}Versions
Resources
Further Reading
See Also
react-web-api/no-leaked-event-listener
Enforces that everyaddEventListenerin a component or custom hook has a correspondingremoveEventListener.react-web-api/no-leaked-fetch
Enforces that everyfetchin a component or custom hook has a correspondingAbortControllerabort in the cleanup function.react-web-api/no-leaked-interval
Enforces that everysetIntervalin a component or custom hook has a correspondingclearInterval.react-web-api/no-leaked-resize-observer
Enforces that everyResizeObservercreated in a component or custom hook has a correspondingResizeObserver.disconnect().