React Native – Lists with FlatList

June 28, 20263 min readUpdated 8/24/2026

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.

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.