Skip to content

v1.0.0

Choose a tag to compare

@vmariev vmariev released this 15 Aug 10:25
· 5 commits to main since this release

A scroll container for React that loads more items as either edge comes into view — with the awkward parts of infinite scrolling already handled.

npm install @vmariev/react-async-list
<AsyncList
  style={{ height: 400 }}
  fetchDown={loadNextPage}
  isDisableFetchDown={!hasMore}
>
  {users.map((user) => (
    <UserRow key={user.id} user={user} />
  ))}
</AsyncList>

Highlights

  • Zero runtime dependencies. Only react as a peer (18 and 19). Nothing to import, no CSS file to remember — styles are injected at runtime, and shipped as @vmariev/react-async-list/styles.css if you'd rather import them yourself.
  • Bidirectional, with each direction independent: a slow fetchUp never blocks fetchDown.
  • Reverse (chat) mode — bottom-anchored, new messages don't move the view, scrolling up loads history.
  • Headless option. useAsyncList hands you the loading engine without any markup, so it drives a <table>, a grid, or a virtualizer just as happily as the component does.
  • Optional custom scrollbar that looks the same on every platform, or the browser's own, or none.
  • ESM + CJS + TypeScript declarations.

It won't flood your API

The one thing worth reading about before you use any infinite-scroll list. A list parked at an edge with a live fetcher is easy to turn into a denial-of-service attack on your own backend, and it needs no user interaction at all:

parked at the edge → fetch → returns nothing (normal, you're at the end)
                   → loading state flips → re-render → re-check
                   → still at the edge → fetch → …forever, ~3 requests/second

This library tracks a content signature per direction and refuses to ask the same question twice: a fetch that comes back empty quiets that direction until the content, the viewport, or a disable flag actually changes. Measured, not assumed — the regression suite asserts an exact request count of one across every configuration, including reverse mode, inline fetchers, and a parent re-rendering faster than the internal throttle.

How loading is triggered →

Reverse mode works on engines that disagree with Chromium

flex-direction: column-reverse changes where the scroll origin sits, and engines differ: most report scrollTop as -max … 0 with 0 at the visual bottom, others use the standard 0 … max. Hard-coding either one silently inverts every write on the engine that disagrees.

So the library doesn't assume. It probes the container once and normalises everything to a single quantity — distance from the visual top. Every reverse-mode behaviour is tested against both conventions.

The negative scrollTop problem →

Working with very long lists

This renders every child, so pair it with a real virtualizer rather than waiting for one to be built in — that's what the headless hook is for. The demo pages 5000 rows through @tanstack/react-virtual while keeping about fifteen of them in the DOM.

With a virtualizer →

Notes

The component grew out of one that had been in production inside a private codebase. Its prop names (deathZone, triggerTopPosition, isHiddenScroll, contentElementId) are still accepted as @deprecated aliases, so existing code keeps compiling while your editor points at the replacements — see Coming from the original component.

A dozen defects carried by the original are fixed here, including a rejected fetch permanently wedging a direction, head-of-line blocking between directions, and a thumb drag that outlived its component and left the whole page stuck with cursor: grabbing. The changelog has the full list.

140 tests, run in CI on Node 20 and 22.

Full documentation: https://github.com/vmariev/react-async-list#readme