Repository navigation
Flashlist v2 initialScrollIndex renders wrong items when item size exceeds 200 #2307
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
Activity
fix(deps): patch FlashList so a deep opening index opens on the right…
eros-piccioni commentedon Sep 8, 2026
Confirming this on a very different shape, and #2444's one-liner fixes it here.
A horizontal
pagingEnabledpager of full-width slides — 420pt each, so above the 200ptdefault — 22 items,
initialScrollIndex: 14. It opens on item 21, the last one.iOS 26.5,
@shopify/flash-list2.3.2, RN 0.85.3, New Architecture.What made this hard to diagnose is that the scroll is correct. Sampling the scroll
handler and the list's own ref on the same frame:
offset === getLayout(14).x === 14 × 420. The list has placed item 14 exactly where thescroll view is. But
contentSizeis 6380 against a true22 × 420 = 9240, and the itemresolved at that offset is 21 — which is your unsorted-layout-table analysis exactly: the
binary search in
getVisibleLayoutsreturns an unrelated index. From outside, every signalreads healthy — the offset is right, and the settled index looks right a frame later — so
this is easy to mistake for a timing problem.
Two things that may help others reproduce it:
because placing the last item recomputes everything; index 5 does not either, because it
fits inside the fully-estimated total. A test aimed at either end passes while a consumer
fails.
Viewthat lays out in the same commit, allitems measured before placement and it never reproduced. Presented by a navigator
(
react-native-screens, transparent modal) it reproduced every time. That lines up withFlashList on Android: item at initialScrollIndex flickers white for one frame on initial render #1887, where the flicker only appears when the list is in a non-primary tab.
#2444 fixes it. Applying only that line on 2.3.2 —
recomputeLayouts(0, initialScrollIndex)→recomputeLayouts(0, this.getDataLength() - 1)— and changing nothing else, the pager opens on the requested item and
contentSizeiscount × itemWidthon the first scroll frame instead of short. That is with 22 items andinitialScrollIndex: 14; I saw the same with 120 items andinitialScrollIndex: 67, thoughthat run had the line applied straight to the installed package rather than through a
committed patch.
This pager also does not reproduce it on 2.0.2, which reads the target offset before
recomputing rather than after — consistent with the behaviour arriving in 2.3.1. I have not
tried 2.0.2 against the 300pt/600-item case in the original report.
test(example): a Carousel and a Gallery that both open a photo full s…