From 50f709d2512d52c76bcc274309cf11b8d430f3f8 Mon Sep 17 00:00:00 2001 From: djk01281 Date: Tue, 3 Feb 2026 22:24:11 +0900 Subject: [PATCH] fix: prevent callback skipping with triggerOnce and merged refs --- src/__tests__/useInView.test.tsx | 39 ++++++++++++++++++++++++++++++++ src/observe.ts | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/__tests__/useInView.test.tsx b/src/__tests__/useInView.test.tsx index f97c0957..d7f7d95c 100644 --- a/src/__tests__/useInView.test.tsx +++ b/src/__tests__/useInView.test.tsx @@ -398,3 +398,42 @@ test("should restore the browser IntersectionObserver", () => { expect(window.IntersectionObserver).toBeDefined(); expect(vi.isMockFunction(window.IntersectionObserver)).toBe(false); }); + +test("should trigger all hooks when using triggerOnce with merged refs", () => { + const MultipleHooksWithTriggerOnce = () => { + const [ref1, inView1] = useInView({ triggerOnce: true }); + const [ref2, inView2] = useInView({ triggerOnce: true }); + const [ref3, inView3] = useInView({ triggerOnce: true }); + + const setRefs = useCallback( + (node: Element | null) => { + ref1(node); + ref2(node); + ref3(node); + }, + [ref1, ref2, ref3], + ); + + return ( +
+
+ {inView1.toString()} +
+
+ {inView2.toString()} +
+
+ {inView3.toString()} +
+
+ ); + }; + + const { getByTestId } = render(); + + mockAllIsIntersecting(true); + + expect(getByTestId("item-1").getAttribute("data-inview")).toBe("true"); + expect(getByTestId("item-2").getAttribute("data-inview")).toBe("true"); + expect(getByTestId("item-3").getAttribute("data-inview")).toBe("true"); +}); diff --git a/src/observe.ts b/src/observe.ts index d75f7e30..1113ba1e 100644 --- a/src/observe.ts +++ b/src/observe.ts @@ -82,7 +82,8 @@ function createObserver(options: IntersectionObserverInit) { entry.isVisible = inView; } - elements.get(entry.target)?.forEach((callback) => { + // Copy the callbacks array before iterating to prevent issues when callbacks are removed during iteration (e.g., when using triggerOnce) + [...(elements.get(entry.target) ?? [])].forEach((callback) => { callback(inView, entry); }); });