How does scrollToLocation address a row in a React Native SectionList, and why can it land on the header or under a sticky header?
answer
- sectionIndex plus itemIndex
- flattened with header and footer slots
- itemIndex 0 is the header slot
- sticky header height added to offset
- needs getItemLayout or a failure handler
basics
~20 sscrollToLocation({sectionIndex, itemIndex}) converts the pair into a flattened index where every section also has a header and footer slot, so itemIndex 0 lands on the section header. With sticky headers it adds the header height to viewOffset for itemIndex above 0.
solid answer
~40 s`scrollToLocation({sectionIndex, itemIndex, viewPosition, viewOffset, animated})` is `SectionList`'s version of `scrollToIndex`. The list computes a flattened index: `itemIndex` plus, for every earlier section, its item count plus two — one slot for the header and one for the footer — and calls `scrollToIndex` on the underlying list. Because a section's header occupies the first slot, **`itemIndex: 0` lands on the section header**, which is exactly what an A-Z sidebar wants when the user taps M, and `itemIndex: n` reaches the section's item `n - 1`. When `stickySectionHeadersEnabled` is on and `itemIndex` is above 0, it adds the section header's measured height to `viewOffset` so the target is not hidden under the pinned header. Like `scrollToIndex`, it can only reach unrendered rows with `getItemLayout` or an `onScrollToIndexFailed` handler, and `getItemLayout` must count header and footer slots.
code
tsx · 47 linesimport {useRef} from 'react';
import {SectionList, Text, View} from 'react-native';
type Artist = {id: string; name: string};
type LetterSection = {key: string; title: string; data: Artist[]};
export function LibraryWithIndex({sections}: {sections: LetterSection[]}) {
const listRef = useRef<SectionList<Artist, LetterSection>>(null);
const jumpToLetter = (letter: string) => {
const sectionIndex = sections.findIndex(s => s.title === letter);
if (sectionIndex >= 0) {
// itemIndex 0 targets the section's header slot.
listRef.current?.scrollToLocation({sectionIndex, itemIndex: 0, viewPosition: 0});
}
};
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<SectionList
ref={listRef}
style={{flex: 1}}
sections={sections}
keyExtractor={a => a.id}
stickySectionHeadersEnabled
renderSectionHeader={({section}) => (
<Text style={{backgroundColor: '#ffffff', fontWeight: '700'}}>{section.title}</Text>
)}
renderItem={({item}) => <Text style={{padding: 10}}>{item.name}</Text>}
onScrollToIndexFailed={info => {
// Flattened index: scroll near it on the underlying list, then retry.
listRef.current?.getScrollResponder()?.scrollTo({
y: info.index * info.averageItemLength,
animated: false,
});
}}
/>
<View>
{sections.map(s => (
<Text key={s.key} onPress={() => jumpToLetter(s.title)}>
{s.title}
</Text>
))}
</View>
</View>
);
}go deeper
Know that SectionList scrolls with scrollToLocation, taking a sectionIndex and an itemIndex, and that far-away sections need getItemLayout or onScrollToIndexFailed.
Explain the flattening: header and footer slots per section, which makes itemIndex 0 the header and shifts items by one. Know viewPosition and viewOffset.
Build a reliable A-Z jump: header-targeted scrolls, per-row targets offset by one, sticky-header compensation, and a bounded fallback for unrendered sections.
Weigh fixed-height grouped rows, which make getItemLayout feasible, against content-driven heights that force retry logic, when designing index-heavy grouped screens.
## The API `SectionList` has no `scrollToIndex` of its own; it has **`scrollToLocation`**: ```tsx listRef.current?.scrollToLocation({ sectionIndex: 12, itemIndex: 0, viewPosition: 0, animated: true, }); ``` The parameters are: - **`sectionIndex`** (required) — which section; - **`itemIndex`** (required) — which position inside that section; - **`viewPosition`** — `0` top, `0.5` middle, `1` bottom; - **`viewOffset`** — extra pixels to offset the final position, for example for an overlapping bar; - **`animated`** — defaults to `true`. ## How the location becomes an index Underneath, a `SectionList` is one virtualized list of cells in which **every section contributes a header slot, its items, and a footer slot** — whether or not you render a header or footer. `scrollToLocation` converts the pair into that flat index: 1. start from `itemIndex`; 2. for each section before `sectionIndex`, add its item count **plus 2**; 3. call the underlying list's `scrollToIndex` with the result. Within a section, the header slot comes first. So the flattened index `prefix + 0` is the **header**, `prefix + 1` is the first item, and so on. The practical meaning: | Call | Where it scrolls | |---|---| | `{sectionIndex: s, itemIndex: 0}` | the header of section `s` | | `{sectionIndex: s, itemIndex: 1}` | the first item of section `s` | | `{sectionIndex: s, itemIndex: n}` | item `n - 1` of section `s` | For an A-Z sidebar in a music library this works out naturally: tapping **M** should bring the **M** header to the top, and `itemIndex: 0` does exactly that. Code that wants a specific artist must add one to the artist's index within its section. ## Sticky headers and the offset With `viewPosition: 0`, the target is placed at the top of the viewport, where a pinned section header would cover it. The docs warn that the item "may be covered by a sticky header". The implementation compensates in one case: - if `stickySectionHeadersEnabled` is on **and** `itemIndex` is greater than 0, it looks up the measured length of the section's header cell and **adds it to `viewOffset`**; - for `itemIndex: 0`, the target is the header itself, so no compensation is needed; - with sticky headers off (Android's default), nothing is added. Any other overlap — a translucent navigation bar, a floating search field — still needs your own `viewOffset`. ## Reaching rows that have not rendered `scrollToLocation` ends in `scrollToIndex`, so it inherits its limits. The docs note you "cannot scroll to locations outside the render window without specifying the `getItemLayout` or `onScrollToIndexFailed` prop": - **`getItemLayout`** lets the list compute offsets without rendering; for a `SectionList` it is called with **flattened indices**, so it must account for header and footer slots, including zero-height footers you never render; - **`onScrollToIndexFailed`** receives the flattened `index`, the highest measured index and the average item length; a common recovery is to scroll close with `scrollTo` on the scroll responder from the list's `getScrollResponder()`, then retry once the rows there have rendered. For a library of a few thousand artists, the second approach is usually simpler than a correct `getItemLayout` for headers, rows and footers. ## Common mistakes - Passing the artist's index within its section as `itemIndex` and landing one row early. - Expecting `itemIndex` to count across the whole list; it is per section. - Writing `getItemLayout` that ignores the header and footer slots, so every offset after the first section is wrong. - Forgetting a `viewOffset` for bars that overlap the list, beyond what sticky-header compensation covers.
- How do you scroll to the fifth artist under M rather than to the M header?Artists within a section start at flattened slot 1, after the header slot, so pass `itemIndex: 5` for the artist at index 4 within the section. With sticky headers enabled, the list adds the M header's height to `viewOffset`, so the artist appears just below the pinned header.
- Why is getItemLayout harder to write for a SectionList than for a FlatList?The list calls it with flattened indices that include a header slot and a footer slot for every section. Your function must map each index back to a header, an item or a footer, and sum their heights, including footers you never render. Getting any of it wrong shifts every later offset.
Think of each section as a chapter that always has a title page and an end page, even when the chapter has no text. scrollToLocation counts pages from the start of the chapter, so page 0 is the title page, the header, and the first real page of text is page 1.
saying these in an interview costs you the question
- itemIndex 0 in scrollToLocation always targets the section's first item.
- itemIndex counts rows across all sections, not within one.
- scrollToLocation can reach any section without getItemLayout or a failure handler.
- Sticky headers never overlap a scrollToLocation target.
- A SectionList's getItemLayout receives section-relative indices.