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 },
});
PropPurpose
dataThe array of items to render — plain JavaScript array, any shape you like
renderItemA function called for each visible item, receiving { item, index }, returning the JSX for that row
keyExtractorA 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

PropPurpose
ListHeaderComponentComponent rendered once, above the first item — e.g. a page title or search bar that scrolls with the list
ListFooterComponentComponent rendered once, after the last item — e.g. a loading spinner while more data is fetched
ListEmptyComponentRendered instead of the list when data is an empty array — e.g. a friendly "No results found" message
onEndReachedCalled 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
onEndReachedThresholdHow close to the end (as a fraction of screen length) before onEndReached fires, e.g. 0.5
horizontalSet 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.

Next up: With lists covered, the next page tackles how users move between different lists and screens — Navigation.