Flashlist v2 initialScrollIndex renders wrong items when item size exceeds 200
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.2k
- Forks
- 393
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 1
Description
Description
FlashList v2 displays wrong items when initialScrollIndex is set and the actual item size exceeds 200px (FlashList's internal averageWindow default). The list opens showing items roughly 83 indices past the requested index.
Current behavior
With initialScrollIndex={250} and items 300px tall, the list opens showing Item 333 instead of Item 250.
The root cause is a race condition in FlashList's layout table construction:
applyInitialScrollAdjustmentruns before any items are measured, building item positions using the defaultaverageWindowof 200px. Item 250 is placed at250 × 200 = 50,000px(should be250 × 300 = 75,000px).- Item 250 renders at the wrong position, measures at 300px, and
modifyLayoutrunsrecomputeLayouts(250→500)— but this anchors items 251+ to item 249's stale 200px-based position (249 × 200 = 49,800px). Items 251+ are now at ~50,400px+. applyInitialScrollAdjustmentfires again with the updatedaverageWindow=300and correctly places items 0–250 ati × 300px. Item 250 moves to 75,000px. However, items 251+ are not touched — they retain their wrong positions from step 2.- The layout table now has a backward jump at the 250→251 boundary (
75,000 → 50,400), making it non-monotonic. - FlashList binary-searches the broken table to find which items to render at scroll position 75,000px. The search starts at mid=249, sees 74,700 < 75,000, goes right — and never checks item 250 (which is to the left). It finds item 333 (at position ~75,000 in the broken layout) and renders that instead.
The bug only manifests when actual item size exceeds 200px. With smaller items (< 200px), the corrective pass moves items to a lower pixel position than what modifyLayout set, creating a forward gap that keeps the table monotonically sorted.
Expected behavior
The list should open with Item 250 visible at the top.
Reproduction
Expo Snack: https://snack.expo.dev/TgCTAo_9hg8zsXmGYrlya
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FlashList } from '@shopify/flash-list';
const ITEM_SIZE = 300;
const INITIAL_INDEX = 250;
const DATA = Array.from({ length: 600 }, (_, i) => i);
export default function App() {
return (
<View style={{ flex: 1 }}>
<View style={styles.header}>
<Text style={styles.title}>Expected: Item {INITIAL_INDEX} at top</Text>
</View>
<FlashList
data={DATA}
renderItem={({ item }) => (
<View style={styles.item}>
<Text style={styles.label}>Item {item}</Text>
</View>
)}
keyExtractor={(item) => String(item)}
initialScrollIndex={INITIAL_INDEX}
/>
</View>
);
}
const styles = StyleSheet.create({
header: { padding: 16, backgroundColor: '#f5f5f5', borderBottomWidth: 1, borderBottomColor: '#ddd' },
title: { fontSize: 16, fontWeight: 'bold' },
item: { height: ITEM_SIZE, justifyContent: 'center', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#eee' },
label: { fontSize: 32, fontWeight: 'bold' },
});
Platform
iOS
Android
Environment
React Native info output:
React Native: 0.81.6
FlashList version: 2.3.1
Additional context
This bug was introduced in FlashList v2 when estimatedItemSize was removed from the public API — a deliberate design decision to eliminate a common source of misconfiguration.
Root of the problem: applyInitialScrollAdjustment runs a self-correcting two-pass loop: the first pass estimates positions using the default averageWindow (200px), and the second corrects them after items are measured. However, modifyLayout fires between the two passes and anchors items after initialScrollIndex to the wrong predecessor position. The second corrective pass then fixes items 0–initialScrollIndex but stops there — leaving a non-monotonic boundary at initialScrollIndex + 1 that breaks the binary search used to select visible items.
Workaround: restore estimatedItemSize and use it to seed the averageWindow so the very first pass produces correct positions, preventing the race condition from occurring. This is not ideal — it reintroduces a prop the maintainers intentionally removed — and there is likely a better internal fix (for example, after the corrective recomputeLayouts(0 → initialScrollIndex), propagating correct positions forward into the items that modifyLayout had incorrectly anchored). I haven't worked out the cleanest approach on the library side, but wanted to document the root cause in detail so it can inform the right fix.
Checklist
I've searched existing issues and couldn't find a duplicate
I've provided a minimal reproduction (Expo Snack preferred)
I'm using the latest version of @shopify/flash-list
I've included all required information above
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the FlashList v2 implementations of applyInitialScrollAdjustment, modifyLayout, and recomputeLayouts, then reproduce the issue using the linked Expo Snack with 300px items and initialScrollIndex={250}. Trace the layout table across both adjustment passes and verify that it remains monotonic and displays Item 250 at the top without relying on estimatedItemSize.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100