Skip to content

fix: re-observe sentinel on data load so IO re-fires when still in trigger zone#430

Open
justanotheranonymoususer wants to merge 2 commits into
ankeetmaini:masterfrom
justanotheranonymoususer:fix-re-observe
Open

fix: re-observe sentinel on data load so IO re-fires when still in trigger zone#430
justanotheranonymoususer wants to merge 2 commits into
ankeetmaini:masterfrom
justanotheranonymoususer:fix-re-observe

Conversation

@justanotheranonymoususer
Copy link
Copy Markdown

No description provided.

…igger zone


IntersectionObserver only fires on intersection state changes. When the
sentinel stayed continuously inside the trigger zone across a data load
(e.g. user holding the End key), no new event fired and the component
got stuck until the user scrolled away and back. Force a fresh callback
by calling unobserve + observe on the existing observer in Effect 2a.

Fixes [ankeetmaini#429](ankeetmaini#429)
Covers issue [ankeetmaini#429](ankeetmaini#429): verifies that on dataLength change the existing
IntersectionObserver has unobserve(sentinel) + observe(sentinel) called
on it (forcing a fresh callback in real browsers) and that the same
observer instance is reused rather than torn down. Both tests fail
against the pre-fix source.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.22%. Comparing base (92b3249) to head (f096b39).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #430      +/-   ##
==========================================
+ Coverage   95.87%   96.22%   +0.35%     
==========================================
  Files           4        4              
  Lines         194      212      +18     
  Branches       65       67       +2     
==========================================
+ Hits          186      204      +18     
  Misses          4        4              
  Partials        4        4              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@iamdarshshah
Copy link
Copy Markdown
Collaborator

Hey @justanotheranonymoususer, thanks for putting up the PR!

Before we merge, could you share a minimal CodeSandbox or StackBlitz reproducing your original setup, specifically the useInfiniteScroll configuration (container height, item sizes, batch size, scrollableTarget or not)? That'll help us validate the fix against your real-world case before shipping.

@justanotheranonymoususer
Copy link
Copy Markdown
Author

I'm using a different computer right now, but it was a standard usage as in the example and I just was holding end. Using Windows. You can't reproduce it without the fix?

@justanotheranonymoususer
Copy link
Copy Markdown
Author

This is my usage in the code:

  const [loadedItems, setLoadedItems] = useState(100);
  //...

  const { sentinelRef } = useInfiniteScroll({
    dataLength: loadedItems,
    next: () =>
      setLoadedItems((prev) =>
        Math.min(prev + 100, dataset.length)
      ),
    hasMore: loadedItems < dataset.length,
    scrollableTarget: 'ContentWrapper',
  });

@iamdarshshah
Copy link
Copy Markdown
Collaborator

We reproduced it locally with useInfiniteScroll. The trigger condition is: each batch must add less total height than the IO's detection zone. With the default scrollThreshold=0.8 on a 500px container, the rootMargin extends 100px (20% of 500px) below the visible edge. A batch of 5 items at 15px each adds 75px, which is less than 100px, so the sentinel never exits the zone after loading. Since IO only fires on intersection changes, it stays silent and the hook gets stuck.

Setup that reproduces it without any keyboard interaction:

const dataset = Array.from({ length: 500 }, (_, i) => i);

const [loadedItems, setLoadedItems] = useState(5); // few enough that sentinel is visible on mount

const { sentinelRef } = useInfiniteScroll({
  dataLength: loadedItems,
  next: () => setLoadedItems((prev) => Math.min(prev + 5, dataset.length)),
  hasMore: loadedItems < dataset.length,
  scrollableTarget: 'ContentWrapper',
});

With a 500px ContentWrapper and 15px items, loading stops after the first batch. With the fix applied (unobserve + observe on every dataLength change), it loads through to the end correctly.

The End key scenario from the original report is the same root cause, just triggered by the user scrolling to the bottom faster than the sentinel can exit and re-enter the zone.

When you get a chance, it would be great if you could share a CodeSandbox or a short recording of your original setup so we can make sure the fix covers your exact case before merging.

@justanotheranonymoususer
Copy link
Copy Markdown
Author

It will take me some time to return to it. You verified that it fixes the issue, and it worked for me when I tried it, and I added tests, so I think it's good to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants