When is flattening grouped data into a FlatList with header rows simpler than a React Native SectionList, and what must you handle yourself?
answer
- one array of headers and items
- renderItem branches on row type
- sticky indices computed by hand
- ListHeaderComponent shifts them by one
- FlashList has no sections API
basics
~20 sFlatten grouped data into one array of header and item rows when the target list has no sections API, such as FlashList, or you want one index space. You then branch in renderItem, keep keys distinct and compute stickyHeaderIndices yourself.
solid answer
~50 s`SectionList` is convenient: sections in, headers, per-platform sticky headers, section separators and `scrollToLocation` out. Flattening into a single array — `[{type: 'header', letter: 'A'}, {type: 'artist', …}, …]` rendered by a `FlatList` — is simpler when the list you are moving to has no sections API (FlashList's own guide migrates a `SectionList` exactly this way), when you want one index space for jumps and viewability, or when other parts of the app already work with flat rows. What you take on: `renderItem` branches on the row type; `keyExtractor` must keep header and item keys distinct; `stickyHeaderIndices` must be computed from the flat array, **plus one when a `ListHeaderComponent` is present**, because the list header occupies the first sticky position; separators, section footers and per-section renderers become your own logic; and empty groups disappear only if you skip them when flattening.
code
tsx · 46 linesimport {useMemo} from 'react';
import {FlatList, StyleSheet, Text} from 'react-native';
type Artist = {id: string; name: string};
type Row =
| {type: 'header'; key: string; letter: string}
| {type: 'artist'; key: string; artist: Artist};
export function FlatArtistList({groups}: {groups: {letter: string; artists: Artist[]}[]}) {
const {rows, stickyHeaderIndices} = useMemo(() => {
const out: Row[] = [];
const sticky: number[] = [];
for (const {letter, artists} of groups) {
if (artists.length === 0) continue; // no empty headers
// +1 because ListHeaderComponent occupies sticky position 0.
sticky.push(out.length + 1);
out.push({type: 'header', key: `h-${letter}`, letter});
for (const artist of artists) {
out.push({type: 'artist', key: `a-${artist.id}`, artist});
}
}
return {rows: out, stickyHeaderIndices: sticky};
}, [groups]);
return (
<FlatList
data={rows}
keyExtractor={row => row.key}
stickyHeaderIndices={stickyHeaderIndices}
ListHeaderComponent={<Text style={styles.title}>Artists</Text>}
renderItem={({item}) =>
item.type === 'header' ? (
<Text style={styles.header}>{item.letter}</Text>
) : (
<Text style={styles.row}>{item.artist.name}</Text>
)
}
/>
);
}
const styles = StyleSheet.create({
title: {fontSize: 28, fontWeight: '700', padding: 12},
header: {backgroundColor: '#ffffff', paddingHorizontal: 12, fontWeight: '700'},
row: {paddingHorizontal: 12, paddingVertical: 10},
});go deeper
Know that grouped data can be rendered either with SectionList or as one flat array of header and item rows in a FlatList.
Explain what flattening costs: branching renderItem, distinct keys, hand-computed stickyHeaderIndices shifted by one when a list header exists, and skipping empty groups.
Choose per screen: SectionList for built-in sticky headers, separators and scrollToLocation; flat rows for a list without a sections API, one index space, or mixed row kinds.
Standardise how grouped data reaches lists, such as a shared selector that emits typed display rows, so screens can switch list implementations without re-deriving grouping.
## Two ways to render grouped data An A-Z music library can be rendered two ways: 1. **`SectionList`** — pass `sections`, each with `data`, and let the list render headers, footers, separators and sticky headers. 2. **A flat list of header rows and item rows** — build one array in which a header row precedes the items of each group, and render it with a `FlatList` (or a recycling list). Both end up as a single virtualized sequence; `SectionList` itself flattens sections into header, item and footer slots internally. The question is who does the flattening, and which features come for free. ## What SectionList gives you | Feature | SectionList | Flat list with header rows | |---|---|---| | Group headers and footers | `renderSectionHeader`, `renderSectionFooter` | rows you add while flattening | | Sticky headers | `stickySectionHeadersEnabled`, per-platform default | `stickyHeaderIndices` you compute | | Separators around groups | `SectionSeparatorComponent` | your own logic in `renderItem` | | Per-group renderers | `renderItem` on a section | a branch on the row type | | Scroll to a group | `scrollToLocation` | `scrollToIndex` with the header row's index | | Row index | per section | one global index | ## When flattening is simpler - **The list component has no sections API.** FlashList's own documentation says it offers none of `SectionList`'s section props and shows migrating a contacts `SectionList` by flattening headers into the data, adding an item type per row and computing `stickyHeaderIndices`. - **You need one index space.** Viewability tracking, analytics on "row N", or a jump bar that maps directly to array positions are easier with a flat array. - **Rows are already flat upstream.** A selector that produces display rows for several screens can hand the same array to any list. - **You want mixed row kinds beyond header and item**, such as an inline banner every 50 artists; a flat array holds any row type. `SectionList` stays simpler when you want its per-platform sticky behaviour, section separators and `scrollToLocation` without writing them. ## What you must handle yourself 1. **Row types.** Give each row a discriminant (`type: 'header' | 'artist'`) and branch in `renderItem`. 2. **Keys.** Header and item rows share one key space: prefix them (`h-A`, `a-123`) so a letter never collides with an id. 3. **Sticky headers.** Compute `stickyHeaderIndices` from the flat array — the positions of header rows. If the list also has a `ListHeaderComponent`, **add one to each index**: the list header occupies the first sticky position, so data index `i` is sticky position `i + 1`. 4. **Empty groups.** Only emit a header when the group has items. 5. **Separators.** `ItemSeparatorComponent` in a `FlatList` renders between every pair of rows, including header-to-item; use `leadingItem` to skip or restyle lines next to headers. 6. **Recompute on change.** The flattened array and the sticky indices must be rebuilt together, for example in one `useMemo`. ## A note on grids `SectionList` has no `numColumns`, and a flat `FlatList` with `numColumns` would put header rows into the grid too. A per-group grid, such as album covers under each letter, needs you to chunk items into row objects yourself in either approach. ## Common mistakes - Computing sticky indices from the data but adding a `ListHeaderComponent`, so the wrong rows pin. - Letting header keys such as `"1"` collide with item ids. - Rebuilding the flattened array on every render without memoizing it. - Recreating `SectionList` features you did not need by flattening by default.
- Why must sticky indices shift by one when a FlatList has a ListHeaderComponent?The list header is rendered as the first cell, and the list checks your sticky indices against positions that count it: position 0 is the list header, so data row `i` is position `i + 1`. Without the shift, each pin lands on the row before the intended header.
- How do you jump to a letter in the flattened version?Record the flat index of each header row while flattening, then call `scrollToIndex` with it on the `FlatList`. The index is a data index, so no list-header shift applies here, and the usual `getItemLayout` or `onScrollToIndexFailed` requirements hold for rows that have not rendered.
saying these in an interview costs you the question
- A FlatList cannot have sticky headers; only SectionList can.
- stickyHeaderIndices in a FlatList ignore ListHeaderComponent.
- Header rows and item rows can reuse the same key values safely.
- Flattening is always faster than SectionList because SectionList is not virtualized.
- SectionList supports numColumns for per-section grids.