Section 2 · Layout and Style
Lists That Stay Fast: FlatList and SectionList
22 min3 min readCode playground
Lesson at a glance
Render Trailhead’s growing hike log with FlatList and SectionList, understand virtualization, and apply the handful of optimizations that keep long lists smooth on mid-range phones.
- 1FlatList: render what’s visible
- 2How virtualization works
- 3Keeping it smooth
- 4SectionList: grouped data
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Trailhead’s log starts with a dozen hikes. A keen hiker logs three a week; after four years that’s over 600 rows, each with a thumbnail, a title and a few stats. Render that with ScrollView and .map(), and the app builds every row’s native views before showing the first frame. On a flagship phone you might not notice. On the mid-range Android phones most of the world uses, you get a blank screen and a stutter.
FlatList: render what’s visible
FlatList takes your data and a function to render one item, and it only mounts the rows near the viewport:
import { FlatList, Text, View } from 'react-native';
import { HikeRow } from '../components/HikeRow';
import { useHikes } from '../state/hikes';
export default function HikeLog() {
const hikes = useHikes();
return (
<FlatList
data={hikes}
keyExtractor={(hike) => hike.id}
renderItem={({ item }) => <HikeRow hike={item} />}
ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: '#e2e8e4', marginLeft: 80 }} />}
ListEmptyComponent={<Text style={{ padding: 24, textAlign: 'center' }}>No hikes yet. Log your first one!</Text>}
contentContainerStyle={{ paddingBottom: 24 }}
/>
);
}Like ScrollView, a FlatList needs a bounded height, so its parent must give it flex: 1 (a screen in Expo Router already does). renderItem receives { item, index }. ListHeaderComponent and ListFooterComponent let a summary card or a “load more” spinner scroll with the list, instead of wrapping the FlatList in a ScrollView, which defeats virtualization entirely and triggers a warning.
How virtualization works
FlatList keeps a render window: the rows on screen plus a buffer above and below. As you scroll, rows entering the window mount and rows leaving it unmount, replaced by empty space of the right height.
Two consequences follow. First, keys must be stable: FlatList and React use them to know which row is which as rows come and go, so use an id from your data. Array indexes break the moment you insert a new hike at the top. Second, rows forget local state when they unmount. An “expanded” flag kept in useState inside the row resets after a long scroll. Lift that state into the parent (expandedId) or into your data.
Keeping it smooth
Most list jank comes from re-rendering rows that didn’t change, or doing heavy work inside them. In order of payoff:
- Memoize the row. Export
HikeRowwrapped inmemo, so a row re-renders only when itshikeprop changes. (If your project enables the React Compiler, it memoizes for you.) - Keep
renderItemstable. An inline arrow function is fine with a memoized row, but don’t create new objects for props on every render, such asstyle={{…}}orhike={{ ...item }}, which defeatmemo. - No heavy work in rows. Format dates and compute stats once, when the data changes, not in every row’s render.
- Right-sized images. A 4000-pixel photo shrunk into a 56-point thumbnail costs memory for every visible row. Request thumbnails from your API, or use
expo-imagewith caching. - Fixed heights? Use
getItemLayout. If every row is 72 points plus a 1-point separator, FlatList can skip measuring:
const ROW = 73;
<FlatList
// …
getItemLayout={(_, index) => ({ length: ROW, offset: ROW * index, index })}
/>For logs that live on a server, don’t fetch all 600 hikes at once. onEndReached fires when the user scrolls near the bottom (how near is set by onEndReachedThreshold, measured in screen heights), which is the moment to request the next page and append it to data. Show a spinner in ListFooterComponent while it loads, and guard against firing the request twice.
Tuning windowSize and initialNumToRender comes last, and only after you’ve measured. For very large or complex feeds, many teams switch to Shopify’s FlashList, which recycles row views instead of mounting new ones and keeps a FlatList-like API.
SectionList: grouped data
Trailhead groups hikes by month, with a sticky header for each. SectionList does that from data shaped like this:
const sections = [
{ title: 'October 2026', data: [ridgeLoop, lakeTrail] },
{ title: 'September 2026', data: [oldMill] },
];<SectionList
sections={sections}
keyExtractor={(hike) => hike.id}
renderItem={({ item }) => <HikeRow hike={item} />}
renderSectionHeader={({ section }) => <Text style={styles.header}>{section.title}</Text>}
stickySectionHeadersEnabled
/>The interesting part isn’t the component; it’s turning a flat array of hikes into that sections shape, correctly sorted. That’s your exercise.
Section 3 starts next: with real screens to move between, Trailhead needs navigation.
Key takeaways
ScrollViewwith.map()mounts every row at once;FlatListmounts only a window of rows around what’s visible.- Give
FlatListakeyExtractorthat returns a stable id from your data, never the array index. - Virtualized rows unmount when they scroll far away, so keep row state in your data, not inside the row component.
- Memoize the row component, keep
renderItemstable, and addgetItemLayoutwhen every row has the same height. SectionListtakessectionsshaped as{ title, data }and renders grouped lists with sticky headers.