no-leaked-intersection-observer
Enforces that every 'IntersectionObserver' created in a component or custom hook has a corresponding 'IntersectionObserver.disconnect()'.
Full Name in eslint-plugin-react-web-api
react-web-api/no-leaked-intersection-observerFull Name in @eslint-react/eslint-plugin
@eslint-react/web-api-no-leaked-intersection-observerPresets
web-api
recommended
recommended-typescript
recommended-type-checked
strict
strict-typescript
strict-type-checked
Rule Details
Creating an IntersectionObserver without disconnecting it can lead to memory leaks and unexpected behavior.
An IntersectionObserver created in useEffect keeps observing its targets after the component unmounts unless it is disconnected in the cleanup function.
Examples
Creating IntersectionObserver without disconnecting in useEffect
import React, { useEffect, useRef } from "react";
function MyComponent() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
// Problem: An 'IntersectionObserver' in 'useEffect' should have a corresponding 'intersectionObserver.disconnect()' in its cleanup function
const observer = new IntersectionObserver(([entry]) => {
console.log("visible:", entry.isIntersecting);
});
observer.observe(ref.current);
}, []);
return <div ref={ref} />;
}Disconnecting IntersectionObserver in useEffect cleanup
import React, { useEffect, useRef } from "react";
function MyComponent() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
// Recommended: Create and use IntersectionObserver
const observer = new IntersectionObserver(([entry]) => {
console.log("visible:", entry.isIntersecting);
});
observer.observe(ref.current);
// Recommended: Disconnect in the cleanup function
return () => observer.disconnect();
}, []);
return <div ref={ref} />;
}Observing multiple elements with IntersectionObserver
When observing multiple elements, disconnect the observer in the cleanup function to release all of them at once.
import { useEffect, useRef } from "react";
function VisibilityTracker() {
const headerRef = useRef<HTMLDivElement>(null);
const footerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Recommended: Disconnect IntersectionObserver when observing multiple elements
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log(entry.target, "intersecting:", entry.isIntersecting);
}
});
if (headerRef.current) observer.observe(headerRef.current);
if (footerRef.current) observer.observe(footerRef.current);
return () => observer.disconnect();
}, []);
return (
<>
<div ref={headerRef}>Header</div>
<div ref={footerRef}>Footer</div>
</>
);
}Using an observe-once IntersectionObserver
A common pattern with IntersectionObserver is to observe an element once and disconnect inside the callback, such as lazy loading content the first time it becomes visible. Disconnecting inside the callback alone is not enough: the component may unmount before the element ever intersects, in which case the callback never runs and the observer leaks.
import { useEffect, useRef, useState } from "react";
function LazySection() {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!ref.current) return;
// Problem: The callback only runs when the element intersects, so the observer leaks if the component unmounts before that
const observer = new IntersectionObserver(([entry], obs) => {
if (entry.isIntersecting) {
setVisible(true);
obs.disconnect(); // observe-once
}
});
observer.observe(ref.current);
}, []);
return <div ref={ref}>{visible ? <p>Loaded</p> : null}</div>;
}import { useEffect, useRef, useState } from "react";
function LazySection() {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry], obs) => {
if (entry.isIntersecting) {
setVisible(true);
obs.disconnect(); // observe-once
}
});
observer.observe(ref.current);
// Recommended: Disconnect in the cleanup function as a fallback in case the component unmounts before the callback runs
return () => observer.disconnect();
}, []);
return <div ref={ref}>{visible ? <p>Loaded</p> : null}</div>;
}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-timeout
Enforces that everysetTimeoutin a component or custom hook has a correspondingclearTimeout.react-web-api/no-leaked-resize-observer
Enforces that everyResizeObservercreated in a component or custom hook has a correspondingResizeObserver.disconnect().