From 5163d331b0a88f05f8b09a376787e25d70e9f035 Mon Sep 17 00:00:00 2001 From: ddussi Date: Tue, 11 Aug 2026 16:08:00 +0900 Subject: [PATCH 1/2] fix: resume InView observation when triggerOnce is disabled --- .../src/InView.tsx | 1 + .../src/__tests__/InView.test.tsx | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/react-intersection-observer/src/InView.tsx b/packages/react-intersection-observer/src/InView.tsx index d130060a..ab159dc5 100644 --- a/packages/react-intersection-observer/src/InView.tsx +++ b/packages/react-intersection-observer/src/InView.tsx @@ -91,6 +91,7 @@ export class InView extends React.Component< prevProps.scrollMargin !== this.props.scrollMargin || prevProps.root !== this.props.root || prevProps.threshold !== this.props.threshold || + prevProps.triggerOnce !== this.props.triggerOnce || prevProps.skip !== this.props.skip || prevProps.trackVisibility !== this.props.trackVisibility || prevProps.delay !== this.props.delay diff --git a/packages/react-intersection-observer/src/__tests__/InView.test.tsx b/packages/react-intersection-observer/src/__tests__/InView.test.tsx index 213a51cf..18624b9b 100644 --- a/packages/react-intersection-observer/src/__tests__/InView.test.tsx +++ b/packages/react-intersection-observer/src/__tests__/InView.test.tsx @@ -144,6 +144,34 @@ test("Should unobserve when triggerOnce comes into view", () => { expect(instance.unobserve).toHaveBeenCalled(); }); +test("Should resume observing when triggerOnce is disabled", () => { + const callback = vi.fn(); + const { rerender } = render( + + Inner + , + ); + + mockAllIsIntersecting(true); + expect(callback).toHaveBeenLastCalledWith( + true, + expect.objectContaining({ isIntersecting: true }), + ); + + rerender( + + Inner + , + ); + callback.mockClear(); + mockAllIsIntersecting(false); + + expect(callback).toHaveBeenLastCalledWith( + false, + expect.objectContaining({ isIntersecting: false }), + ); +}); + test("Should unobserve when unmounted", () => { const { container, unmount } = render(Inner); const instance = intersectionMockInstance(container.children[0]); From 3bc4847a25c1b37e846b8366ec12ffeba347d117 Mon Sep 17 00:00:00 2001 From: ddussi Date: Mon, 24 Aug 2026 09:59:23 +0900 Subject: [PATCH 2/2] test: cover triggerOnce transitions while in view --- .../src/__tests__/InView.test.tsx | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/packages/react-intersection-observer/src/__tests__/InView.test.tsx b/packages/react-intersection-observer/src/__tests__/InView.test.tsx index 18624b9b..971e9d0c 100644 --- a/packages/react-intersection-observer/src/__tests__/InView.test.tsx +++ b/packages/react-intersection-observer/src/__tests__/InView.test.tsx @@ -144,32 +144,103 @@ test("Should unobserve when triggerOnce comes into view", () => { expect(instance.unobserve).toHaveBeenCalled(); }); -test("Should resume observing when triggerOnce is disabled", () => { +test("Should resume observing when triggerOnce is disabled while in view", () => { const callback = vi.fn(); - const { rerender } = render( + const { container, rerender } = render( Inner , ); + const element = container.children[0]; + const initialObserver = intersectionMockInstance(element); + vi.spyOn(initialObserver, "unobserve"); mockAllIsIntersecting(true); - expect(callback).toHaveBeenLastCalledWith( + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenNthCalledWith( + 1, true, expect.objectContaining({ isIntersecting: true }), ); + expect(initialObserver.unobserve).toHaveBeenCalledWith(element); + callback.mockClear(); rerender( Inner , ); - callback.mockClear(); - mockAllIsIntersecting(false); + const resumedObserver = intersectionMockInstance(element); + expect(resumedObserver).not.toBe(initialObserver); - expect(callback).toHaveBeenLastCalledWith( + // A fresh observer reports the element's current state. + mockAllIsIntersecting(true); + expect(callback).toHaveBeenNthCalledWith( + 1, + true, + expect.objectContaining({ isIntersecting: true }), + ); + + mockAllIsIntersecting(false); + expect(callback).toHaveBeenNthCalledWith( + 2, false, expect.objectContaining({ isIntersecting: false }), ); + + mockAllIsIntersecting(true); + expect(callback).toHaveBeenNthCalledWith( + 3, + true, + expect.objectContaining({ isIntersecting: true }), + ); + expect(callback).toHaveBeenCalledTimes(3); +}); + +test("Should stop observing when triggerOnce is enabled while in view", () => { + const callback = vi.fn(); + const { container, rerender } = render( + + Inner + , + ); + const element = container.children[0]; + const initialObserver = intersectionMockInstance(element); + vi.spyOn(initialObserver, "unobserve"); + + mockAllIsIntersecting(true); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenNthCalledWith( + 1, + true, + expect.objectContaining({ isIntersecting: true }), + ); + + callback.mockClear(); + rerender( + + Inner + , + ); + expect(initialObserver.unobserve).toHaveBeenCalledWith(element); + + const triggerOnceObserver = intersectionMockInstance(element); + expect(triggerOnceObserver).not.toBe(initialObserver); + vi.spyOn(triggerOnceObserver, "unobserve"); + + // A fresh observer reports the element's current state before triggerOnce stops it. + mockAllIsIntersecting(true); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenNthCalledWith( + 1, + true, + expect.objectContaining({ isIntersecting: true }), + ); + expect(triggerOnceObserver.unobserve).toHaveBeenCalledWith(element); + + mockAllIsIntersecting(false); + mockAllIsIntersecting(true); + expect(callback).toHaveBeenCalledTimes(1); }); test("Should unobserve when unmounted", () => {