From 6ff366b854f5c077786e89d842b16e70d32bd516 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Tue, 14 Jul 2026 13:10:00 +0200 Subject: [PATCH] fix: make observe cleanup idempotent --- src/__tests__/observe.test.ts | 26 ++++++++++++++++++++++++++ src/observe.ts | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/src/__tests__/observe.test.ts b/src/__tests__/observe.test.ts index 40841e01..d40175c8 100644 --- a/src/__tests__/observe.test.ts +++ b/src/__tests__/observe.test.ts @@ -19,6 +19,32 @@ test("should be able to use observe", () => { ); }); +test("should only clean up each observer callback once", () => { + const element = document.createElement("div"); + const firstCallback = vi.fn(); + const secondCallback = vi.fn(); + const firstCleanup = observe(element, firstCallback, { threshold: 0.1 }); + const secondCleanup = observe(element, secondCallback, { threshold: 0.1 }); + const observer = intersectionMockInstance(element); + + firstCleanup(); + firstCleanup(); + mockIsIntersecting(element, true); + + expect(firstCallback).not.toHaveBeenCalled(); + expect(secondCallback).toHaveBeenCalledTimes(1); + expect(observer.unobserve).not.toHaveBeenCalled(); + + secondCleanup(); + expect(observer.unobserve).toHaveBeenCalledTimes(1); + expect(observer.unobserve).toHaveBeenCalledWith(element); + expect(observer.disconnect).toHaveBeenCalledTimes(1); + + secondCleanup(); + expect(observer.unobserve).toHaveBeenCalledTimes(1); + expect(observer.disconnect).toHaveBeenCalledTimes(1); +}); + test("should convert options to id", () => { expect( optionsToId({ diff --git a/src/observe.ts b/src/observe.ts index 0e84176b..529d073b 100644 --- a/src/observe.ts +++ b/src/observe.ts @@ -156,7 +156,11 @@ export function observe( callbacks.push(callback); observer.observe(element); + let unobserved = false; return function unobserve() { + if (unobserved) return; + unobserved = true; + // Remove the callback from the callback list callbacks.splice(callbacks.indexOf(callback), 1);