Shopify / Shopify/flash-list

Flashlist v2 initialScrollIndex renders wrong items when item size exceeds 200

Open
#2,307 1 comment 7 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

agent-triage bug P1
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:

  1. 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).
  2. 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+.
  3. 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.
  4. The layout table now has a backward jump at the 250→251 boundary (75,000 → 50,400), making it non-monotonic.
  5. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.