Lists are most of what a mobile app shows. React Native gives you two ways to render one, and the choice matters far more than it does on the web.
ScrollView renders everything
A ScrollView mounts every child immediately and keeps them all in memory, whether
they are on screen or not. With a dozen items that is fine. With five hundred it means five hundred
views constructed before the first frame — a list that takes seconds to open and then scrolls
badly.
FlatList virtualises: it renders the rows near the viewport and
recycles them as you scroll. The API is barely longer, so the habit is worth forming even on short
lists.
Use ScrollView for a screen of mixed content — a form, a details page, a checkout.
Use FlatList for a repeating collection of unknown length.
The minimum
<FlatList
data={orders}
keyExtractor={(order) => order.id}
contentContainerStyle={styles.content}
refreshing={refreshing}
onRefresh={handleRefresh}
ListHeaderComponent={data and renderItem are the two required props.
keyExtractor is not required and you should always supply it — it plays the role
key does in a React list, and without it React Native falls back to the array index,
which reorders badly and breaks recycling.
Note renderItem receives an object, not the item:
({ item }) => …. Forgetting to destructure is the first mistake everybody makes.
Pull to refresh, in two props
refreshing={refreshing}
onRefresh={handleRefresh}That is the whole implementation. It matters more than it sounds: there is no browser reload
button on a phone, so a list that fetches once and never again is a list the user cannot fix when it
goes stale. refreshing is a boolean you own; onRefresh fires when the user
drags down.
This is one of the clearest arguments for FlatList over a hand-rolled
ScrollView — you would otherwise be wiring a gesture and a spinner yourself.
Header, footer and empty states
A screen's title usually belongs inside the list, not above it, so it scrolls away with the content instead of pinning:
ListEmptyComponent={
<EmptyState title="Nothing here yet" message="No products match this filter." />
}
renderItem={({ item }) => <ProductCard product={item} onSelect={handleSelect} />}
showsVerticalScrollIndicator={false}
/>ListEmptyComponent renders when data is empty and is the easiest
loading/empty/error branch to forget. An empty list that renders as blank space reads as a broken
screen; a sentence explaining why reads as a working one.
ListHeaderComponent and ListFooterComponent do the same for the ends.
The footer is where a "loading more" spinner goes when you paginate.
Grids
numColumns={2}
key="two-column"
columnWrapperStyle={styles.column}numColumns turns the list into a grid, and columnWrapperStyle styles
each row of it — which is where the gap between columns goes.
⚠️ numColumns cannot change in place. FlatList throws if you change
it on an existing list, because the whole recycling arrangement depends on it. The fix is a
key on the list itself, so a change remounts it rather than mutating it. Hard-coding
key="two-column" documents that the value is fixed; if it were dynamic, the key would
be too.
Keeping it fast
Virtualisation only helps if rendering a row is cheap. Three things matter, in order:
Memoise the row component. Anything that re-renders the screen re-renders every
visible row unless the row is wrapped in memo and its props are stable. That means the
callback you pass to renderItem has to be stable too — which is why the demo app wraps
handleSelect in useCallback. One without the other achieves nothing.
Lesson 21 goes into this properly.
Give rows a fixed height if you can. getItemLayout lets FlatList
skip measuring, which makes scrolling to an index instant and long-list scrolling smoother. It only
works when every row really is the same height — lying about that produces jumping.
Do not put a FlatList inside a ScrollView in the same
direction. It disables virtualisation entirely — the list gets infinite height to work with, so it
renders everything — and React Native warns about it. If you need a list under other content, that
content belongs in ListHeaderComponent.
SectionList, and the newer options
SectionList is the same idea with grouped data and sticky section headers — an
address book, or orders grouped by month.
For very large or very heavy lists, FlashList from Shopify is a drop-in replacement
with better recycling. It is worth reaching for when you have measured a problem, and not before:
FlatList handles ordinary app-sized lists perfectly well.
What is next
TextInput and Forms — where a mobile app is usually won or lost.