Skip to content

InView does not resume observing when triggerOnce changes to false #781

Description

@ddussi

Describe the bug

Hi! I noticed what looks like a lifecycle inconsistency in the class-based
<InView> component.

After <InView triggerOnce> receives an intersecting entry, it stops observing
as expected. If the same mounted component is then rerendered with
triggerOnce={false}, observation does not appear to resume, so later leave and
enter transitions are not reported to onChange.

From what I can tell, <InView> reinitializes its observer when options such as
root, threshold, and skip change, but componentDidUpdate() does not
currently include triggerOnce. This also seems inconsistent with the hook
lifecycle, where triggerOnce participates in recreating the observer callback.

To reproduce

The linked issue-template CodeSandbox currently uses
react-intersection-observer@9.3.3, so here is a minimal reproduction against
the current repository test suite:

test("Should resume observing when triggerOnce is disabled", () => {
  const callback = vi.fn();
  const { rerender } = render(
    <InView triggerOnce onChange={callback}>
      Inner
    </InView>,
  );

  mockAllIsIntersecting(true);
  expect(callback).toHaveBeenLastCalledWith(
    true,
    expect.objectContaining({ isIntersecting: true }),
  );

  rerender(
    <InView triggerOnce={false} onChange={callback}>
      Inner
    </InView>,
  );
  callback.mockClear();
  mockAllIsIntersecting(false);

  expect(callback).toHaveBeenLastCalledWith(
    false,
    expect.objectContaining({ isIntersecting: false }),
  );
});

On the current main branch, the final callback is not invoked and the
assertion fails.

Expected behavior

I would expect observation to resume after triggerOnce changes to false, so
that later leave and enter transitions can be reported.

Screenshots

Not applicable; the failing callback assertion above captures the behavior.

Desktop

  • OS: macOS 26.5 (25F71)
  • Browser: Not browser-specific; reproduced with the repository's Vitest
    IntersectionObserver mock
  • Browser version: Not applicable

Smartphone

Not tested / not applicable.

Additional context

  • Confirmed in v11.0.0 and on current main (a25b644).
  • This appears to be a long-standing class-component lifecycle behavior rather
    than a recent regression.
  • React: 19.2.8 in the repository test suite

The relevant lifecycle path seems to be that handleChange() invokes and clears
the stored unobserve callback when inView && triggerOnce. Since
componentDidUpdate() does not compare triggerOnce, changing it from true
to false alone does not call observeNode() again.

I also checked #455/#456, #466, #512, and #758/#765. They seem to cover generic
re-observation, dynamic observer options, or hook-only work rather than this
class-component transition.

I opened #782 with a small lifecycle change and regression test. If runtime
updates to triggerOnce are intentionally unsupported for the class-based
<InView>, or if another approach would fit the project better, I’d be happy to
adjust it. Thanks for taking a look!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions