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)
Summary
A horizontal
rtlLegendListopened with aninitialScrollIndexnear the end settles on the RTL mirror index instead of the requested one.initialScrollIndex={594}→ item 595 (of 604)TOTAL - 1 - 594 = index 9).Minimal repro repo: https://github.com/Mohamed-kassim/legend-list-rtl-repro (run
npm install && npx expo run:android)@legendapp/list@3.0.6)Environment
@legendapp/list3.0.60.85.319.2.3Steps to reproduce
A horizontal,
rtl, pagedLegendListof 604 fixed-width pages opened atinitialScrollIndex={594}: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)
rtl={false}recycleItems={false}initialScrollIndexRoot 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:When the watchdog wins the race against the bootstrap scroll's native
onScrollround-trip (more likely on a large jump, a slow device, or withrecycleItems), it dispatches the unconverted logical offset — which in RTL maps to the mirror position. TheinitialContentOffsetseed for the first nativecontentOffsetis also taken unconverted for RTL.Fix
Proposed in #478 — converts the offset in
scrollToFallbackOffsetand skips the unconverted RTLinitialContentOffsetseed, mirroring the normaldoScrollTodispatch. 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)