FlatList / SectionList
FlatList renders large scrollable lists efficiently — virtualises off-screen rows, only mounts what’s near the viewport. Use it for any list with more than ~20 items.
Render, pull-refresh, infinite, sectioned
EXAMPLE
import { useCallback, useEffect, useRef, useState } from 'react';
import {
FlatList,
SectionList,
RefreshControl,
View,
Text,
Image,
StyleSheet,
ActivityIndicator,
Pressable,
} from 'react-native';
// 1) Basic FlatList
function Posts({ posts }) {
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.body} numberOfLines={2}>{item.body}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.sep} />}
/>
);
}
// 2) Real feed — pull-to-refresh + infinite scroll + empty state
function Feed() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [refresh, setRefresh] = useState(false);
const cursor = useRef(null);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
const r = await api.feed({ after: cursor.current, limit: 20 });
setPosts(p => cursor.current === null ? r.items : [...p, ...r.items]);
cursor.current = r.nextCursor;
setHasMore(!!r.nextCursor);
setLoading(false);
}, [loading, hasMore]);
const onRefresh = useCallback(async () => {
setRefresh(true);
cursor.current = null;
setHasMore(true);
await loadMore();
setRefresh(false);
}, [loadMore]);
useEffect(() => { loadMore(); }, []);
return (
<FlatList
data={posts}
keyExtractor={(it) => it.id}
renderItem={({ item }) => <PostRow post={item} />}
// Pull to refresh
refreshControl={<RefreshControl refreshing={refresh} onRefresh={onRefresh} />}
// Infinite scroll
onEndReached={loadMore}
onEndReachedThreshold={0.5}
// Loading + empty
ListEmptyComponent={!loading && (
<View style={styles.empty}>
<Text>No posts yet</Text>
</View>
)}
ListFooterComponent={loading && (
<View style={styles.footer}>
<ActivityIndicator />
</View>
)}
ItemSeparatorComponent={() => <View style={styles.sep} />}
// Performance
removeClippedSubviews
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
/>
);
}
// 3) Memoised row component — prevents needless re-renders on parent change
import { memo } from 'react';
const PostRow = memo(function PostRow({ post }) {
return (
<Pressable style={styles.row}>
<Image source={{ uri: post.avatar }} style={styles.avatar} />
<View style={{ flex: 1 }}>
<Text style={styles.title}>{post.title}</Text>
<Text style={styles.body} numberOfLines={2}>{post.body}</Text>
</View>
</Pressable>
);
});
// 4) Fixed-height items — huge perf win (no measure pass)
<FlatList
data={posts}
getItemLayout={(_, index) => ({ length: 80, offset: 80 * index, index })}
renderItem={renderItem}
/>
// 5) Scroll to position
const ref = useRef();
ref.current?.scrollToIndex({ index: 100, animated: true });
ref.current?.scrollToOffset({ offset: 0, animated: true });
ref.current?.scrollToEnd({ animated: true });
<FlatList ref={ref} ... />
// 6) Header / footer / sticky headers
<FlatList
ListHeaderComponent={<TopBar />}
ListFooterComponent={<Footer />}
stickyHeaderIndices={[0]} // 0 = the header sticks
data={posts}
...
/>
// 7) Horizontal list
<FlatList
horizontal
data={stories}
renderItem={({ item }) => <StoryCircle uri={item.uri} />}
showsHorizontalScrollIndicator={false}
/>
// 8) SectionList — grouped data
function Chats({ sections }) {
return (
<SectionList
sections={sections} // [{ title: 'Today', data: [...] }, { title: 'Yesterday', data: [...] }]
keyExtractor={(item) => item.id}
renderItem={({ item }) => <MessageRow msg={item} />}
renderSectionHeader={({ section }) => (
<Text style={styles.sectionHeader}>{section.title}</Text>
)}
stickySectionHeadersEnabled
/>
);
}
// 9) Performance checklist
// - keyExtractor returns a STABLE id (not the index for reorderable lists)
// - memo'd row component
// - getItemLayout for fixed-height rows (skip measure pass)
// - removeClippedSubviews on Android
// - Tune maxToRenderPerBatch, initialNumToRender, windowSize, updateCellsBatchingPeriod
// - Avoid inline functions in renderItem prop — use useCallback
// - Don't put heavy logic in renderItem — pre-compute in parent
// - Use FastImage instead of Image for many remote images
// 10) Replace FlatList for highest-perf needs
// FlashList (Shopify) is a drop-in faster alternative:
// import { FlashList } from '@shopify/flash-list';
// <FlashList data={...} renderItem={...} estimatedItemSize={80} />
// 2-3x faster scroll on long lists; 60fps on lower-end devices.
// 11) Common bugs
// - Using index as the key — broken on reorder/remove
// - onEndReached firing on initial mount → infinite recursion if loadMore doesn't bail
// - Heavy work in renderItem — every row re-renders + janks
// - Forgetting onEndReachedThreshold — scroll-to-bottom never fires soon enough
// - Mixing FlatList inside ScrollView — virtualisation breaks
const styles = StyleSheet.create({
row: { flexDirection: 'row', gap: 12, padding: 12, alignItems: 'center' },
avatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#eee' },
title: { fontWeight: '600', fontSize: 15 },
body: { color: '#475569', fontSize: 13 },
sep: { height: 1, backgroundColor: '#e5e7eb' },
empty: { padding: 24, alignItems: 'center' },
footer: { padding: 16, alignItems: 'center' },
sectionHeader: { padding: 8, backgroundColor: '#f8fafc', fontWeight: '600' },
});
Why it matters
For any list past ~20 items, FlatList with stable keyExtractor + memoised row + getItemLayout (when heights are fixed) keeps 60fps scroll. FlashList is the drop-in upgrade when you need more.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { FlatList, Text } from 'react-native';
<FlatList
data={users}
keyExtractor={u => u.id}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
Try it Yourself »
Exercise
Key prop FlatList uses to identify rows.
<FlatList data={xs}
={u => u.id} renderItem={…} />
camelCase.
Discussion
Loading…