v1.0.0
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
reactas 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.cssif you'd rather import them yourself. - Bidirectional, with each direction independent: a slow
fetchUpnever blocksfetchDown. - Reverse (chat) mode — bottom-anchored, new messages don't move the view, scrolling up loads history.
- Headless option.
useAsyncListhands 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.
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.
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