react-native-virtual-list

A virtualized list for React Native and react-native-web that keeps its scroll position when item heights are only known after they render.

latest version on npm downloads per month CI status date of last release date of last commit MIT licence

npm install react-native-virtual-list

Why your list jumps when you load older messages

You open a chat in the middle, the user scrolls up, you fetch the previous page and prepend it. The content above the viewport grows, and the whole list lurches down by the height of everything you just inserted. The user loses their place.

On React Native the platform solves this for you. ScrollView has a maintainVisibleContentPosition prop that records the frame of the first visible child before a mount commit and shifts contentOffset by the difference afterwards. It never consults your estimates, so a bad estimate cannot break it. FlashList turns it on by default.

react-native-web does not implement that prop. Not partially, not with caveats. The string maintainVisibleContentPosition does not appear anywhere in react-native-web 0.21.2. On web, every list has to hold its own position, and most do not.

That is the gap this library exists for. It tracks an item and how far its top sits above the viewport, rather than a scroll offset, so measurement and insertion cannot move what you are looking at.

Use it

import { VirtualList } from 'react-native-virtual-list'

<VirtualList
  data={messages}
  estimatedItemHeight={72}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Message {...item} />}
  onStartReached={loadOlder}
/>

When older messages arrive, tell the list how many went on the front. It is explicit rather than inferred from data growing, because only you know which end changed, and guessing wrong is what makes a list jump.

ref.current.prepend(50)

ref.current.scrollToIndex(4823, { align: 'center', animated: true })
ref.current.scrollToEnd()

Item positions cannot go out of order

Offsets here are prefix sums over a Fenwick tree, not an array of positions that gets partially rebuilt. Heights are never negative, so the sequence of offsets is non decreasing by construction. There is no table that can fall out of sort order, so the search that decides which rows are visible cannot be misled.

That is not a hypothetical benefit. In Shopify/flash-list#2307, unmeasured items are positioned from a rolling average seeded at 200px. When the rows above finally measure taller, their positions grow but the rows below keep the stale average, the position table steps backwards, and the binary search over it returns an unrelated row. Ask for item 250, render item 333. This repository contains a test that reproduces that mechanism and then shows it cannot occur here.

What is actually tested

PropertyHow it is checked
Prefix sums and searches are correctDifferential fuzzing against a naive O(n) implementation over 2000 random updates
Offsets never step backwardsMeasured in adversarial arrival orders
indexAt inverts offsetOfEvery index across a list of one million
The anchored row never moves40 random trials, measuring above, below and on the anchor
Prepending never moves the anchor30 random trials, including buffer reallocation
Totals do not driftSum of a million Float32 heights equals the tree total exactly
Only visible rows mount13 render tests through react-test-renderer; 100,000 rows mount fewer than 15

56 automated tests, no device needed. Plus a manual pass on a physical iOS device, and a browser pass on react-native-web where a probe read the real getBoundingClientRect().top of a tracked row across four consecutive prepends to 2000 rows. Every round the scroll position moved by exactly the amount the content grew.

Cost at scale

1M items5M items
Build11 ms52 ms
visibleRange0.84 µs1.15 µs
setHeight0.33 µs0.29 µs
Prepend 500.09 ms0.02 ms
Index memory12 MB58 MB

A frame at 60fps is 16.7 ms. Deciding what is visible at five million items uses about 0.007 percent of it. The index costs 13 bytes per row, which at a million rows is less than the data you are listing.

Where you should not use this

If you are React Native only and FlashList works, use FlashList. It has a team behind it, per type recycling pools, and far more users. This does not try to beat it and will not.

Release history

VersionDateWhat changed
0.1.1 17 Aug 2026 Fixed web prepending, which was broken in 0.1.0. The layout maths was right but the corrected offset was never written back to the scroll view, so web jumped by the full inserted height. Also ships compiled output so bundlers that only treat .jsx as JSX can build it.
0.1.0 15 Aug 2026 First release.

Issues and pull requests are welcome, particularly a case where the position still moves. A reproduction naming the platform and the sequence of calls is enough.