Skip to content

Fix SectionList.scrollToLocation no-op for itemIndex: 0#57594

Closed
KAMRONBEK wants to merge 1 commit into
react:mainfrom
KAMRONBEK:fix/50143-sectionlist-scrolltolocation-itemindex0
Closed

Fix SectionList.scrollToLocation no-op for itemIndex: 0#57594
KAMRONBEK wants to merge 1 commit into
react:mainfrom
KAMRONBEK:fix/50143-sectionlist-scrolltolocation-itemindex0

Conversation

@KAMRONBEK

Copy link
Copy Markdown

Summary

SectionList.scrollToLocation({ sectionIndex, itemIndex: 0 }) silently does
nothing: no scroll happens, no error is logged, and onScrollToIndexFailed
never fires. itemIndex: 1 (and higher) works. This is most visible on iOS,
where stickySectionHeadersEnabled defaults to true. Fixes #50143.

Root cause

VirtualizedSectionList flattens all sections into a single VirtualizedList.
Every section contributes a header cell and a footer cell around its items, so
the flat-index layout for a section starting at flat offset base is:

base + 0     -> section header
base + 1     -> item (relative index 0)   <-- the "first row"
...
base + n     -> item (relative index n-1)
base + n + 1 -> section footer

(This layout is authoritatively defined by _subExtractor, which subtracts 1
for the header when resolving an item's relative index.)

Before this change, scrollToLocation mapped { sectionIndex, itemIndex } to a
flat index without accounting for the current section's header:

let index = params.itemIndex;                  // missing the header offset
for (let i = 0; i < params.sectionIndex; i++)
  index += getItemCount(sections[i].data) + 2; // header + items + footer of prior sections

So the mapping was off by one:

  • itemIndex: 0 resolved to base → the section header, not the first row.
  • itemIndex: 1 resolved to base + 1 → the first row (relative item 0).

itemIndex: 0 therefore targeted the header. With sticky headers enabled the
header is already pinned at the top of the viewport, so scrolling to its offset
lands where the viewport already is — a visually imperceptible no-op. And because
that header cell is typically already measured, scrollToIndex does not take its
onScrollToIndexFailed branch — hence "no scroll, no error, no callback", exactly
as reported. The sticky-header compensation block made the same off-by-one
assumption and was additionally gated on itemIndex > 0, so it skipped
itemIndex: 0 entirely.

The public API documents scrollToLocation as scrolling to "the item at the
specified sectionIndex and itemIndex"
(SectionList.d.ts), so itemIndex: 0
should target the first item, not the header.

Root cause: packages/virtualized-lists/Lists/VirtualizedSectionList.js,
scrollToLocation (the let index = params.itemIndex seed and the sticky block).

The fix

  1. Add + 1 when seeding the flat index so itemIndex: 0 maps to the first row
    (base + 1) rather than the section header.
  2. Apply the sticky-header height compensation for itemIndex >= 0 (was > 0)
    so itemIndex: 0 also lands below the pinned header; the header's flat index
    is correspondingly index - params.itemIndex - 1.

Out-of-range indices are now forwarded to VirtualizedList.scrollToIndex instead
of silently resolving to a header/footer cell, so its existing range validation
and onScrollToIndexFailed path run as documented.

Behavior-change note (for reviewers)

This corrects a long-standing off-by-one dating to commit 8a82503b5 (2019),
which locked {sectionIndex: 0, itemIndex: 0} -> index: 0 into a regression test
with a // prevents #18098 comment. That change's actual goal (per its
description) was to stop viewOffset from being overridden — the index: 0
value was incidental, not the point of the test. #18098 was about scrollToLocation
overscrolling past content on the last section; the viewOffset-preservation
guarantee that fixed it is retained here, and only the index/header mapping is
corrected. Existing scrollToLocation test expectations were updated to the
corrected semantics.

Changelog:

[General] [Fixed] - SectionList.scrollToLocation now scrolls to itemIndex: 0
(the first row of a section) instead of silently no-op'ing on the sticky section
header.

Test Plan

Added/updated tests in
packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js:

  • Updated the existing scrollToLocation expectations to the corrected mapping
    (indices +1; itemIndex: 0 no longer resolves to the header).
  • itemIndex: 0 with sticky headers → index: 1 with the header height added to
    viewOffset (regression test for SectionList.scrollToLocation not scrolling to section with itemIndex: 0 #50143).
  • itemIndex: 0 in a later section → that section's first row
    ({sectionIndex: 1, itemIndex: 0} -> index: 6), not its header.
  • An out-of-range itemIndex is still forwarded to scrollToIndex (so
    onScrollToIndexFailed can fire) rather than being swallowed.

Ran the suite locally (Node 24):

yarn jest packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js
→ 14 passed, 14 total

Confirmed the tests catch the bug: reverting the source change makes the 7
scrollToLocation expectations fail; restoring it makes them pass.

scrollToLocation flattened {sectionIndex, itemIndex} to a VirtualizedList
index without accounting for the current section's header cell, so
itemIndex: 0 resolved to the section header instead of the first row.
With sticky headers (iOS default) that header is already pinned to the
top, so the scroll was a visually imperceptible no-op and, because the
header cell is usually already measured, onScrollToIndexFailed never
fired -- exactly the report in react#50143.

Seed the flat index with `itemIndex + 1` so itemIndex: 0 targets the
first item (the row after the header), matching the documented "item at
the specified sectionIndex and itemIndex". Apply the sticky-header height
compensation for itemIndex >= 0 (was > 0) so the first row is not
obscured by the pinned header. Out-of-range indices are now forwarded to
scrollToIndex so its range validation / onScrollToIndexFailed path runs.

Updates the existing scrollToLocation expectations to the corrected
mapping and adds regression tests for react#50143 (sticky itemIndex: 0,
later-section itemIndex: 0, and out-of-range forwarding).

Fixes react#50143
@meta-cla

meta-cla Bot commented Jul 17, 2026

Copy link
Copy Markdown

Hi @KAMRONBEK!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@KAMRONBEK KAMRONBEK closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SectionList.scrollToLocation not scrolling to section with itemIndex: 0

1 participant