FlatList and SectionList are thin layers over VirtualizedList, which you can use directly when your data is not an array. It asks for the data through two functions, getItemCount(data) and getItem(data, index), so the "data" can be anything: a database cursor, an immutable collection, or a formula. This catalog has 100,000 books that never exist at once; each is made when the list asks for it:
const source = { count: 100_000, at: (i: number) => catalog(1, i)[0] };
let made = 0;
const getItem = (s: Source, i: number) => { made++; return s.at(i); };
const onViewable = ({ viewableItems: v }: { viewableItems: ViewToken<Book>[] }) =>
console.log(`visible ${v[0]?.index}-${v.at(-1)?.index}, items made so far: ${made}`);
// ...
<VirtualizedList<Book> data={source} getItem={getItem} getItemCount={(s) => s.count}
keyExtractor={(b) => String(b.id)} renderItem={({ item }) => <BookRow book={item} />}
getItemLayout={(_, i) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * i, index: i })}
initialScrollIndex={50_000} onViewableItemsChanged={onViewable} />Opening the list, then one swipe, logged:
LOG visible 50000-50009, items made so far: 59 LOG visible 50003-50012, items made so far: 1429
Because every row is ROW_HEIGHT tall, getItemLayout lets the list compute any offset without rendering, so it opened at book 50,000 directly. The count of getItem calls is the lesson: the list asks for items far more often than it renders them, while building its window and for every key, so getItem must be cheap. onViewableItemsChanged (with viewabilityConfig) reports what is on screen, for analytics or autoplay.