Lists: FlatList & SectionList
Almost every real app has at least one long, scrollable list somewhere — a feed, a contact
directory, a product catalog, search results. React Native gives you dedicated, high-performance
components for exactly this job: FlatList for flat lists, and SectionList
for lists grouped under headings. Both solve a specific performance problem that a naive approach
runs straight into.
Why Not Just ScrollView + .map()?
It's tempting to render a list the same way you might in plain React on the web: wrap a
ScrollView around an array's .map() output. This works for a handful
of items, but it has a serious problem at scale:
// Works, but do NOT do this for long or dynamic lists
<ScrollView>
{items.map(item => (
<Text key={item.id}>{item.title}</Text>
))}
</ScrollView>
A ScrollView renders every single item immediately, all at once —
including the hundreds of items the user hasn't scrolled to yet and may never see. For a list of
10 items, that's harmless. For a list of 5,000 items (a chat history, a product catalog, an
infinite feed), it means allocating memory and rendering work for thousands of off-screen
components up front, which shows up as slow startup, laggy scrolling, and high memory usage.
FlatList: Virtualized, Performant Lists
FlatList solves this with virtualization: it only renders the
items currently near the visible screen area, plus a small buffer just outside it. As the user
scrolls, items that move far off-screen are recycled/unmounted, and new items entering the
visible area are rendered just in time. The result: a list of 5,000 items performs about as well
as a list of 20, because only a small window of them is ever actually mounted.
import React from 'react';
import { FlatList, Text, View, StyleSheet } from 'react-native';
const PRODUCTS = [
{ id: '1', name: 'Wireless Mouse' },
{ id: '2', name: 'Mechanical Keyboard' },
{ id: '3', name: 'USB-C Hub' },
];
export default function ProductList() {
return (
<FlatList
data={PRODUCTS}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.name}>{item.name}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 14, borderBottomWidth: 1, borderBottomColor: '#eeeeee' },
name: { fontSize: 16 },
});
| Prop | Purpose |
|---|---|
data | The array of items to render — plain JavaScript array, any shape you like |
renderItem | A function called for each visible item, receiving { item, index }, returning the JSX for that row |
keyExtractor | A function returning a unique string ID for each item |
keyExtractor matters for the same underlying reason the plain key prop
matters when mapping over an array in ordinary React: it gives React (and here, FlatList's
virtualization engine) a stable, unique identifier per item, so it can correctly
track which rendered row corresponds to which data item across re-renders, insertions, removals,
and reordering — instead of just guessing based on position, which can cause rows to flicker,
lose internal state, or show stale content.
Common FlatList Props Reference
| Prop | Purpose |
|---|---|
ListHeaderComponent | Component rendered once, above the first item — e.g. a page title or search bar that scrolls with the list |
ListFooterComponent | Component rendered once, after the last item — e.g. a loading spinner while more data is fetched |
ListEmptyComponent | Rendered instead of the list when data is an empty array — e.g. a friendly "No results found" message |
onEndReached | Called when the user scrolls near the end of the list — the standard hook for implementing infinite-scroll pagination by fetching the next page of data |
onEndReachedThreshold | How close to the end (as a fraction of screen length) before onEndReached fires, e.g. 0.5 |
horizontal | Set to true to scroll left-to-right instead of top-to-bottom |
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderProduct}
ListEmptyComponent={
<Text style={styles.emptyText}>No products found.</Text>
}
onEndReached={loadNextPage}
onEndReachedThreshold={0.5}
/>
SectionList: Grouped Lists
When items need to be grouped under headings — a contacts list grouped by first letter, a menu
grouped by category — SectionList is the tool for it. Instead of a flat
data array, it takes a sections array, where each section has its own
title and its own data:
import React from 'react';
import { SectionList, Text, View, StyleSheet } from 'react-native';
const CONTACTS = [
{ title: 'A', data: ['Aditi', 'Anil'] },
{ title: 'S', data: ['Sonu', 'Sunita', 'Suresh'] },
{ title: 'V', data: ['Vikas'] },
];
export default function ContactsScreen() {
return (
<SectionList
sections={CONTACTS}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => (
<View style={styles.row}>
<Text>{item}</Text>
</View>
)}
renderSectionHeader={({ section }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{section.title}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 12 },
header: { backgroundColor: '#f0f0f0', paddingHorizontal: 12, paddingVertical: 6 },
headerText: { fontWeight: 'bold' },
});
renderSectionHeader is the section-list-specific addition: a function that renders
the sticky-feeling heading above each group, given that group's section object
(which includes the title you defined).
Pull-to-Refresh, Built Right In
"Pull down to refresh" is such a common pattern that FlatList (and
SectionList) support it directly through two props — no separate wrapper component
or third-party "refresh indicator" needed:
import React, { useState, useCallback } from 'react';
import { FlatList, Text } from 'react-native';
export default function FeedScreen() {
const [items, setItems] = useState(initialItems);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
const fresh = await fetchLatestItems();
setItems(fresh);
setRefreshing(false);
}, []);
return (
<FlatList
data={items}
keyExtractor={item => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
refreshing={refreshing}
onRefresh={onRefresh}
/>
);
}
refreshing is a boolean telling the list whether to show its built-in spinner right
now, and onRefresh is called automatically when the user performs the pull-down
gesture — you just flip refreshing to true, do your data fetch, then
flip it back to false when done. This is notably simpler than the equivalent in
some other cross-platform frameworks (like Cordova, where pull-to-refresh typically means adding
and wiring up a separate plugin or wrapper component) — here it's two props on the same list
you're already rendering.