Skip to content

Horizontal RTL: initialScrollIndex near the end settles on the mirror index #476

Description

@Mohamed-kassim

Summary

A horizontal rtl LegendList opened with an initialScrollIndex near the end settles on the RTL mirror index instead of the requested one.

  • Requested: initialScrollIndex={594} → item 595 (of 604)
  • Actual: the list briefly reports item 595, then settles on item 10 — the RTL mirror (TOTAL - 1 - 594 = index 9).

Minimal repro repo: https://github.com/Mohamed-kassim/legend-list-rtl-repro (run npm install && npx expo run:android)

Bug (@legendapp/list@3.0.6) With the fix below
journey: 595 → 10          journey: 595
BUG — settled on item 10   OK — settled on item 595

Environment

@legendapp/list 3.0.6
react-native 0.85.3
react 19.2.3
expo SDK 56
platform Android (emulator)

Steps to reproduce

A horizontal, rtl, paged LegendList of 604 fixed-width pages opened at initialScrollIndex={594}:

<LegendList
  data={DATA}                     // 604 items
  horizontal
  rtl                             // ← required: bug is RTL-specific
  pagingEnabled
  bounces={false}
  recycleItems                    // ← reliably makes the watchdog win the race
  initialScrollIndex={594}        // ← large jump toward the end amplifies it
  getFixedItemSize={() => WIDTH}
  estimatedListSize={{ width: WIDTH, height: HEIGHT }}
  renderItem={...}
/>

Observed: settles on item 10 (the RTL mirror). Deterministic — 5/5 cold starts.
Expected: settles on item 595 (the requested index).

Isolation (each independently avoids it)

change settles on
(as above) 10 — mirror
rtl={false} 595 ✓ — confirms it is RTL-specific
recycleItems={false} 595 ✓ — confirms the initial-scroll race is the trigger
small initialScrollIndex 595 ✓ — small jumps settle before the watchdog fires

Root cause

Legend List keeps logical (LTR) scroll offsets internally and converts to the native RTL coordinate only at the native boundary (toNativeHorizontalOffset). The normal dispatch (doScrollTo) converts correctly, but the initial-scroll watchdog/retry path does not:

// scrollToFallbackOffset (3.0.6)
function scrollToFallbackOffset(ctx, offset) {
  ctx.state.refScroller.current?.scrollTo({
    animated: false,
    x: ctx.state.props.horizontal ? offset : 0,   // ← logical offset, UNCONVERTED
    y: ctx.state.props.horizontal ? 0 : offset,
  });
}

When the watchdog wins the race against the bootstrap scroll's native onScroll round-trip (more likely on a large jump, a slow device, or with recycleItems), it dispatches the unconverted logical offset — which in RTL maps to the mirror position. The initialContentOffset seed for the first native contentOffset is also taken unconverted for RTL.

Fix

Proposed in #478 — converts the offset in scrollToFallbackOffset and skips the unconverted RTL initialContentOffset seed, mirroring the normal doScrollTo dispatch. With it, this repro settles on the requested item; tsc/lint/tests pass.

(Minimal toggleable repro: https://github.com/Mohamed-kassim/legend-list-rtl-repro)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions