DEV Community

Cover image for Why Most React Infinite Scroll Hooks Fail in Production (and the One That Fixed It for Me)
Shubhra Pokhariya
Shubhra Pokhariya

Posted on

Why Most React Infinite Scroll Hooks Fail in Production (and the One That Fixed It for Me)

I used to believe infinite scroll was one of the simplest features to implement.

Fetch data → append to a list → load more on scroll.

Easy, right?

That’s exactly how most tutorials show it.

And honestly… it works.

Until it doesn’t.

The Bug That Changed Everything

One bug completely changed how I think about infinite scroll.

A user changed a filter → the list reset → new data loaded.

Everything looked correct.

Then a couple of seconds later…

the old results came back and overwrote the new ones.

No errors. No warnings.

Just silently broken UI.

What actually happened?

A slow request from the previous state finished late and updated the UI with stale data.

That was the moment I realized:

Infinite scroll doesn’t break in demos.It breaks with real users, real timing, and real networks.

Where Things Start Falling Apart

After that, I started noticing the same issues again and again:

  • Stale requests overwriting fresh data after reset
  • Retry logic fetching the wrong page
  • IntersectionObserver continuing to fire after errors
  • Duplicate requests in React Strict Mode
  • Loading states that never resolve

These problems rarely show up locally.

But in production, they show up fast.

What I Actually Needed

Not another demo.

Something I could trust in a real app.

So I built a hook around one idea:

Control the async flow. Don’t just “load more data”

What This Hook Handles

  • ✔ Cancels stale requests (no overwrite bugs)
  • ✔ Smart retry logic (correct page every time)
  • ✔ Proper observer cleanup (no leaks)
  • ✔ React 18+ Strict Mode safe
  • ✔ Clear initial vs pagination loading states
  • ✔ Manual loadMore, reset, and retry
  • ✔ Zero dependencies

Basic Usage

const {
  items,
  isInitialLoading,
  hasMore,
  error,
  loadMoreRef,
  retry,
  reset
} = useInfiniteScroll({
  fetchFn: async (page, signal) => {
    const res = await fetch(`/api/posts?page=${page}`, { signal });
    return res.json();
  },
  pageSize: 20,
});
return (
  <div>
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>

    {/* Sentinel element */}
    <div ref={loadMoreRef} />

    {error && <button onClick={retry}>Retry</button>}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Real Use Case: Reset on Filter Change

This is where most bugs happen.

useEffect(() => {
  reset(); // cancels old request + reloads safely
}, [filter]);
Enter fullscreen mode Exit fullscreen mode

Without proper handling, this is exactly where stale data bugs appear.

Why AbortController Matters So Much

Before using it:

old requests still resolved
stale data overwrote the UI

After using it:

old requests get cancelled immediately
they never reach your state

That single change removes an entire class of bugs.

One Honest Note

This hook handles most real-world issues.

But for very large lists, you should still use virtualization
(like react-window or tanstack/virtual).

Infinite scroll solves loading.

Virtualization solves rendering.

Try It Yourself

You can grab the full hook here:

👉 https://shubhra.dev/snippets/use-infinite-scroll

MIT licensed
free to copy and modify
built for real production cases

Final Thought

I didn’t build this because infinite scroll is hard.

I built it because the small edge cases are what break real products.

Slow APIs. Fast users. Overlapping requests.

That’s where things actually fall apart.

Curious - what broke for you?

Have you ever hit a weird infinite scroll bug in production?

Something like:

  • duplicate data
  • stale UI
  • loading that never stops

Would genuinely love to hear what you ran into.

Top comments (0)