Why can scrollToIndex on a React Native FlatList throw or fail for a far-away row, and how do you make it reliable?
answer
- unrendered rows have no measurements
- getItemLayout or onScrollToIndexFailed
- averageItemLength in the failure info
- scroll near, then retry
- initialScrollIndex runs once after layout
basics
~20 sFlatList only knows the offsets of rows it has measured. scrollToIndex to an unmeasured row throws unless getItemLayout or onScrollToIndexFailed is set; the failure callback lets you scroll near the row and retry once it renders.
solid answer
~40 s`scrollToIndex({index, animated, viewPosition, viewOffset})` has to turn an index into a pixel offset. With `getItemLayout`, the list computes it; without it, the list only knows rows it has already measured. For an index beyond the highest measured cell, it throws `scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed…` — unless you pass `onScrollToIndexFailed`, which receives `{index, highestMeasuredFrameIndex, averageItemLength}`. The usual recovery for a contacts list jumping to letter M: `scrollToOffset` to `index * averageItemLength`, then retry `scrollToIndex` once the rows there have rendered. Out-of-range indices always throw. `viewPosition` places the row at the top (0), middle (0.5) or bottom (1). `initialScrollIndex` uses the same machinery once, after the content is laid out, and the docs say it requires `getItemLayout`.
code
tsx · 44 linesimport {useRef} from 'react';
import {FlatList, Text} from 'react-native';
type Contact = {id: string; name: string};
export function ContactsWithIndex({contacts}: {contacts: Contact[]}) {
const listRef = useRef<FlatList<Contact>>(null);
const retries = useRef(0);
const jumpToLetter = (letter: string) => {
const index = contacts.findIndex(c => c.name.startsWith(letter));
if (index >= 0) {
retries.current = 0;
listRef.current?.scrollToIndex({index, animated: true, viewPosition: 0});
}
};
return (
<FlatList
ref={listRef}
data={contacts}
keyExtractor={c => c.id}
renderItem={({item}) => <Text style={{padding: 12}}>{item.name}</Text>}
onScrollToIndexFailed={info => {
// Land near the row using the measured average, then retry.
listRef.current?.scrollToOffset({
offset: info.index * info.averageItemLength,
animated: false,
});
if (retries.current < 3) {
retries.current += 1;
setTimeout(() => {
listRef.current?.scrollToIndex({index: info.index, animated: true});
}, 100);
}
}}
ListHeaderComponent={
<Text onPress={() => jumpToLetter('M')} style={{padding: 12}}>
Jump to M
</Text>
}
/>
);
}go deeper
Know that scrollToIndex needs either getItemLayout or onScrollToIndexFailed for rows that have not been rendered yet, and that indices must be within the data.
Explain the mechanism: the list turns an index into an offset from getItemLayout or from measured cells, and throws or calls the failure callback when it has neither. Know viewPosition and viewOffset.
Implement a robust jump for variable-height rows: scroll near by averageItemLength, retry after rendering with a cap, and account for multi-column row indices and initialScrollIndex's requirements.
Weigh fixed row heights, which make every index addressable, against content-driven heights that need retry logic, when designing list-heavy screens with jump navigation.
## The scenario A 300-row contacts list has an alphabet index on the right. Tapping **M** should jump to the first contact starting with M, which is row 180. The list has rendered only the first screenful or two. The call is: ```tsx listRef.current?.scrollToIndex({index: 180, animated: true}); ``` It throws an invariant error instead of scrolling. ## Why it fails `FlatList` forwards `scrollToIndex` to the underlying `VirtualizedList`, which must convert the index into a content offset. It has two sources of truth: 1. **`getItemLayout`**, if you provided it: a function returning `{length, offset, index}` for any index, so offsets are known without rendering. 2. **Measured cells**: the list records the layout of every cell it has rendered in a metrics cache. Without `getItemLayout`, a row that has never been rendered has no measurement. The implementation checks for exactly this: if `getItemLayout` is missing and the index is beyond the highest measured cell, then: - if `onScrollToIndexFailed` is **not** set, it throws: `scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, otherwise there is no way to know the location of offscreen indices or handle failures.`; - if it **is** set, it calls it with `{index, highestMeasuredFrameIndex, averageItemLength}` and returns without scrolling. Independently, the index must be in range: a negative index, an empty list or an index past the end throw `scrollToIndex out of range…` regardless of the other props. ## Making it reliable | Approach | When it fits | |---|---| | `getItemLayout` | rows have a known, fixed size (including separators) | | `onScrollToIndexFailed` with scroll-and-retry | rows vary in height or are measured at runtime | | `scrollToOffset` with your own offset | you already store offsets, for example from section headers | The prop's own documentation gives the recommended action: "either compute your own offset and `scrollTo` it, or scroll as far as possible and then try again after more items have been rendered". A common implementation: 1. in `onScrollToIndexFailed`, call `scrollToOffset({offset: info.index * info.averageItemLength, animated: false})` to land near the target; 2. once the rows around that offset have rendered (a short timeout or the next layout), call `scrollToIndex` again; 3. cap the retries so a list that never measures the row cannot loop. `getItemLayout` itself, and its interaction with windowing, is a tuning topic of its own; here it matters as the prop that makes every index addressable. ## Positioning the target - **`viewPosition`**: `0` puts the row at the top, `1` at the bottom, `0.5` in the middle. - **`viewOffset`**: a fixed number of pixels subtracted from the target, for example to leave room for a sticky search bar overlapping the list. - **`animated`**: defaults to `true`. - **`scrollToItem`** takes an item instead of an index but does a linear scan of `data`; prefer `scrollToIndex`. ## initialScrollIndex `initialScrollIndex` opens the list at a row instead of the top — for example, reopening the contacts list at the contact the user last viewed. Things to know: - after the content has been laid out, the list scrolls to that index once, without animation, using the same `scrollToIndex` path; - it disables the optimisation that keeps the first `initialNumToRender` rows always rendered and instead renders from the initial index; - the docs state it **requires `getItemLayout`**; without it the jump depends on measurements that do not exist yet; - an index outside the data logs a warning that it is not valid. ## Common mistakes - Calling `scrollToIndex` without either `getItemLayout` or `onScrollToIndexFailed`. - Retrying forever in `onScrollToIndexFailed`. - Forgetting separator heights in offsets you compute yourself. - Passing an item index in a multi-column list, where indices count rows.
- Why does scrollToIndex work for row 5 but fail for row 180 in the same list?Row 5 has been rendered, so the list has its measured offset. Row 180 has never been rendered, and without `getItemLayout` the list has no offset for it: it throws, or calls `onScrollToIndexFailed` if you provided it. The difference is measurement, not distance as such.
- When does getItemLayout make the failure callback unnecessary?When every row's size is known up front, for example fixed-height contact rows plus a fixed separator. The list then computes any offset directly and never needs to have rendered the target. Variable-height rows cannot supply accurate layouts, which is when scroll-and-retry is needed.
saying these in an interview costs you the question
- scrollToIndex works for any index as soon as the list mounts.
- onScrollToIndexFailed fires only for indices past the end of data.
- initialScrollIndex works reliably without getItemLayout for variable rows.
- viewPosition 1 puts the target row at the top of the viewport.
- Retrying scrollToIndex in a loop until it succeeds is safe.