iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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={…} />

Test yourself

Q1. FlatList is preferred over ScrollView+map for…
Q2. A required prop is…
Q3. A unique row ID goes in…

Discussion

Loading…