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:
applyInitialScrollAdjustment runs before any items are measured, building item positions using the default averageWindow of 200px. Item 250 is placed at 250 × 200 = 50,000px (should be 250 × 300 = 75,000px).
- Item 250 renders at the wrong position, measures at 300px, and
modifyLayout runs recomputeLayouts(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+.
applyInitialScrollAdjustment fires again with the updated averageWindow=300 and correctly places items 0–250 at i × 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
Description
FlashListv2 displays wrong items wheninitialScrollIndexis set and the actual item size exceeds 200px (FlashList's internalaverageWindowdefault). 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).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.75,000 → 50,400), making it non-monotonic.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
modifyLayoutset, 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
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