Skip to content

React Native FlatList vs ScrollView: What to Use and When

React Native FlatList vs ScrollView: when to use each, a side-by-side table, the FlatList props that fix slow lists, and common mistakes.
GuideUpdated 8 min readDevelopmentMobile AppsReact Js

Use FlatList for any list built from data, especially one that can grow, because it only renders the rows near the screen. Use ScrollView for short, fixed content such as a settings screen, a form or a detail page, because it renders every child at once and that is only cheap while there are few children.

That is the whole decision for most screens. The rest of this guide shows how each one works, the FlatList props that fix a slow list, the mistakes that cause most performance complaints, and where SectionList and FlashList fit. It is the same checklist I use on client apps as part of my React Native development work, and every prop below is checked against the official React Native documentation.

FlatList vs ScrollView at a glance

ScrollViewFlatList
RenderingEvery child, as soon as it mountsRows near the viewport, rendered in batches as you scroll
MemoryGrows with every item you addBounded by windowSize (21 screen lengths by default)
Large lists (hundreds of rows or more)Slow to mount and heavy to keepWhat it is built for
Headers and footersOrdinary children above or below the contentListHeaderComponent, ListFooterComponent and ListEmptyComponent
Pull to refreshrefreshControl with a RefreshControl (vertical only)refreshing and onRefresh
Sticky headersstickyHeaderIndicesstickyHeaderIndices, or SectionList for sticky section headers
Infinite scrollHand-rolled from scroll eventsonEndReached and onEndReachedThreshold
Typical useSettings, forms, profile and detail screensFeeds, chats, search results, anything from an API

How ScrollView works, and when to use it

ScrollView is a scrollable container. The React Native docs are direct about the trade-off: it renders all of its children at once, and on a long list that means creating components and native views for content the user may never scroll to, which slows rendering and raises memory use.

For short content that is not a problem, and ScrollView is the simpler tool. Use it when:

  • The number of children is small and bounded, and will stay that way
  • The content is a layout rather than a list, such as a form, a profile or a product page
  • You want full control over what sits inside the scrollable area
import { ScrollView, Text } from 'react-native';

export function Settings({ options }) {
  return (
    <ScrollView>
      {options.map((option) => (
        <Text key={option.id}>{option.label}</Text>
      ))}
    </ScrollView>
  );
}

One thing that catches people out: a ScrollView needs a bounded height to work. If it does not scroll, check that every parent view passes flex: 1 down the tree rather than setting a fixed height on the ScrollView itself.

ScrollView is not simply a worse FlatList. FlatList does real work to virtualise: it tracks which rows are visible, renders in batches and mounts and unmounts rows as you scroll. On a screen of ten fixed sections that machinery buys nothing, and a plain ScrollView is easier to lay out, easier to read and easier to debug.

How a React Native FlatList works

FlatList is built on VirtualizedList. It renders an initial batch of rows (initialNumToRender, 10 by default), then renders more in small batches as the user scrolls, and unmounts rows that move far enough outside a render window. That is why a list of thousands of rows can mount as quickly as a list of twenty.

It also brings the features a data list usually needs, so you do not build them yourself: pull to refresh, infinite scroll, separators, an empty state, headers and footers, and multiple columns through numColumns. A typical production list looks like this:

import { memo, useCallback } from 'react';
import { FlatList, Text, View } from 'react-native';

const Row = memo(function Row({ title }) {
  return (
    <View style={{ height: 64, justifyContent: 'center' }}>
      <Text>{title}</Text>
    </View>
  );
});

export function OrdersList({ orders, refreshing, onRefresh, onLoadMore }) {
  const renderItem = useCallback(({ item }) => <Row title={item.title} />, []);

  return (
    <FlatList
      data={orders}
      renderItem={renderItem}
      keyExtractor={(item) => String(item.id)}
      refreshing={refreshing}
      onRefresh={onRefresh}
      onEndReached={onLoadMore}
      onEndReachedThreshold={0.5}
      ListEmptyComponent={<Text>No orders yet</Text>}
    />
  );
}

Two details matter here. onEndReachedThreshold is measured in visible lengths of the list, so 0.5 asks for more data when the end is within half a screen; the default is 2. And because FlatList is a PureComponent, it only re-renders when its props change. If a row depends on something outside data, such as a selected id, pass it through extraData or the row will not update.

One more consequence of virtualisation: a row's internal state is not preserved once it scrolls out of the render window and is unmounted. If a row has a toggle, a text input or an expanded state the user expects to keep, store that state in the item data or in a parent store rather than in useState inside the row.

The React Native FlatList props that actually matter

FlatList is faster than ScrollView out of the box, but most complaints about it being slow come down to a handful of props left at their defaults on a list that needed them set.

PropWhat it doesSet it when
keyExtractorReturns a unique string key per row, used for caching and re-ordering. The default checks item.key, then item.id, then falls back to the indexYour items have no key or id field, or the id is a number you want as a string
getItemLayoutTells FlatList each row's length and offset so it can skip measuringEvery row is the same height (or width, for a horizontal list)
initialNumToRenderHow many rows render in the first batch. Default 10The first screen is slow to appear, or 10 rows does not fill it
windowSizeHow many visible lengths of content stay rendered. Default 21: the screen plus up to 10 above and 10 belowFast scrolling shows blank space (raise it) or memory is too high (lower it)
maxToRenderPerBatchRows rendered per batch while scrolling. Default 10You need to trade fill rate against responsiveness to taps
removeClippedSubviewsDetaches off-screen views from the native hierarchy. Default true on Android, false elsewhereLong lists, after testing; the docs warn it can cause missing content

If every row has a fixed height, getItemLayout is usually the first prop to add, because without it FlatList has to measure rows as it goes. Remember to include the separator height in the offset if you use ItemSeparatorComponent.

const ROW_HEIGHT = 64;

const getItemLayout = (data, index) => ({
  length: ROW_HEIGHT,
  offset: ROW_HEIGHT * index,
  index,
});

<FlatList data={orders} renderItem={renderItem} getItemLayout={getItemLayout} />

Common FlatList vs ScrollView mistakes

Using ScrollView for hundreds of items

This is the most frequent cause of a slow screen. Mapping an API response into a ScrollView works in development with twenty test records, then mounts every row at once when production returns hundreds. If the data comes from a server and has no fixed ceiling, start with FlatList even while the list is short.

Defining renderItem inline

Writing renderItem as an arrow function inside the JSX creates a new function on every render of the parent. The React Native performance guide recommends moving it out of the returned JSX and wrapping it in useCallback, as in the example above, and memoising the row component with memo so unchanged rows are skipped.

Missing or unstable keys

If your items have neither key nor id, FlatList falls back to the array index. Index keys break as soon as rows are inserted, removed or re-ordered, which shows up as the wrong row keeping its state or animation. Give every item a stable id and return it as a string from keyExtractor.

Nesting a FlatList inside a ScrollView

Putting a FlatList inside a ScrollView that scrolls the same way triggers this warning in development: "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead." The outer ScrollView gives the list unlimited height, so it renders everything and you lose the virtualisation you chose it for.

The fix is almost always to invert it. Make the FlatList the outer component and move the content that sat above and below it into ListHeaderComponent and ListFooterComponent:

<FlatList
  data={orders}
  renderItem={renderItem}
  keyExtractor={(item) => String(item.id)}
  ListHeaderComponent={<ProfileHeader />}
  ListFooterComponent={<Footer />}
/>

The header scrolls with the list, the list stays virtualised, and there is only one vertical scroll on screen. A horizontal FlatList inside a vertical ScrollView, such as a carousel, does not trigger the warning, because the orientations differ.

Moving a ScrollView list to FlatList

Most of the lists I am asked to speed up started life as a map inside a ScrollView. The change is small and safe if you take it in order:

  1. Replace the ScrollView and the map with a FlatList, passing the array as data and the row markup as renderItem.
  2. Add a keyExtractor that returns a stable id as a string, and remove the key you were setting on each mapped child.
  3. Move anything that sat above or below the list into ListHeaderComponent and ListFooterComponent, so there is only one scroll container.
  4. Extract the row into its own component wrapped in memo, and wrap renderItem in useCallback.
  5. If a row reads state that is not in data, pass that value to extraData.
  6. Add getItemLayout if rows are a fixed height, then test on a lower-end Android device rather than only in a simulator.

What about SectionList?

SectionList is the built-in choice when your data falls into groups with headings: contacts by letter, orders by date, bookings by month. It is virtualised in the same way, takes the same performance props, and adds renderSectionHeader. Section headers stick to the top by default on iOS and not on Android; stickySectionHeadersEnabled controls that.

<SectionList
  sections={[
    { title: 'Today', data: todayOrders },
    { title: 'Earlier', data: olderOrders },
  ]}
  keyExtractor={(item) => String(item.id)}
  renderItem={({ item }) => <Row title={item.title} />}
  renderSectionHeader={({ section }) => <Text>{section.title}</Text>}
/>

When FlatList is still not fast enough: FlashList

FlashList is Shopify's open-source list component with a FlatList-style API. Instead of mounting and unmounting rows, it recycles row components as you scroll. The current major version, v2, was rebuilt for React Native's New Architecture and will not run on the old one; projects still on the old architecture need v1. Unlike v1, v2 does not need size estimates, so the old estimatedItemSize prop is no longer used.

import { FlashList } from '@shopify/flash-list';

<FlashList
  data={orders}
  renderItem={renderItem}
  keyExtractor={(item) => String(item.id)}
/>

Its docs strongly recommend a valid keyExtractor, and getItemType helps recycling when a list mixes different row types. Because rows are reused, any useState inside a row can carry over to a different item, so check row state when you migrate. It is also one more dependency to keep current. Most lists that feel slow on FlatList improve once the mistakes above are fixed, so fix those and measure before switching.

Frequently asked questions

How many items before I should switch from ScrollView to FlatList?

There is no official number. The better question is whether the count is bounded. A settings screen has a fixed set of rows, so ScrollView is fine. Anything driven by an API has no ceiling, so use FlatList from the start, even while the list is short.

Why is my FlatList showing blank space while I scroll?

Rows are rendering more slowly than you are scrolling. Add getItemLayout if rows are a fixed height, memoise the row component, move renderItem into useCallback, use smaller images, and consider raising windowSize or maxToRenderPerBatch, accepting the extra memory and JavaScript work.

Can FlatList have sticky headers and pull to refresh?

Yes. refreshing and onRefresh add pull to refresh, and FlatList accepts stickyHeaderIndices because it inherits ScrollView props. For headers that belong to groups of rows, SectionList is the cleaner tool.

Summary: which to use

Your situationUse
A settings screen with a dozen rowsScrollView
A form or detail page that scrollsScrollView
A feed, a chat, a product listFlatList
Anything paginated from an APIFlatList
Grouped rows with section headersSectionList
Very long or complex lists still dropping frames after tuning, on the New ArchitectureConsider FlashList

Choosing between FlatList and ScrollView is a decision about how the screen will behave in production, not about syntax. Pick ScrollView for short, fixed content and FlatList for data, and the most common performance problems never appear.

If you would rather have the lists, and the app around them, built and tuned for you, see my mobile app development in Dubai services.

Share this
App work

Need the app actually built?

I build iOS and Android apps in React Native, with the back-end and the store submission included rather than left as somebody else's problem. A first conversation costs nothing.

Not ready to book a call? Get a free website health check