EthicsComputerCourses
Join training +91 90532 10052

Lists with FlatList

React Native · Lesson 9 of 30 Beginner 6 min read Updated

By the end of this lesson you will be able to show hundreds or thousands of items smoothly with FlatList. You will add separators, an empty message, pull to refresh, loading more items at the bottom, and a two-column grid.

Lists are everywhere in apps: chats, products, contacts, train results. Getting them right is one of the most important mobile skills.

Why not just map inside a ScrollView?

ScrollView builds every child at once. With 2,000 contacts, the phone creates 2,000 rows before showing anything. That is slow and uses a lot of memory.

FlatList is virtualised. It only creates the rows that are on screen, plus a few above and below. As you scroll, rows that leave the screen are removed and new ones are created. It is like a railway ticket counter that only prints the tickets for the people standing in the queue now, not for every passenger of the year.

Step 1: your first FlatList

FlatList needs two props: data (an array) and renderItem (a function that draws one item). keyExtractor tells it how to find a unique key for each item, just like key in Lists & Keys.

React JSX
import { FlatList, View, Text, StyleSheet } from "react-native";

const trains = [
  { id: "12015", name: "Ajmer Shatabdi", from: "New Delhi", to: "Ajmer", departs: "06:10" },
  { id: "12951", name: "Mumbai Rajdhani", from: "Mumbai Central", to: "New Delhi", departs: "17:00" },
  { id: "16526", name: "Island Express", from: "Kanyakumari", to: "Bengaluru", departs: "10:30" },
];

export default function Trains() {
  return (
    <FlatList
      data={trains}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={styles.row}>
          <Text style={styles.name}>{item.id} {item.name}</Text>
          <Text>{item.from} to {item.to}, departs {item.departs}</Text>
        </View>
      )}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
    />
  );
}

const styles = StyleSheet.create({
  row: { padding: 16 },
  name: { fontWeight: "700", marginBottom: 4 },
  separator: { height: 1, backgroundColor: "#e5e5e5", marginLeft: 16 },
});

What you should see: three train rows, each with a bold title and a route line, separated by thin grey lines. Notice that renderItem receives an object, and the data item is inside it as item. It also receives index if you need the position.

Real lists need a title at the top, something at the bottom, and a friendly message when there is nothing to show.

React JSX
<FlatList
  data={results}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <TrainRow train={item} />}
  ListHeaderComponent={<Text style={{ padding: 16, fontSize: 20 }}>Trains to Jaipur</Text>}
  ListEmptyComponent={<Text style={{ padding: 16 }}>No trains found. Try another date.</Text>}
  ListFooterComponent={<View style={{ height: 32 }} />}
  contentContainerStyle={{ paddingBottom: 16 }}
/>

What you should see: with an empty results array, the header and the "No trains found" message appear. With data, the rows appear under the header. Because the header is part of the list, it scrolls away with the rows.

Practise the logic: search and sort

Lists usually come with a search box and a sort button. The list on screen is derived from the full data plus the search text. The calculation is plain JavaScript, so try it in the browser. In your app you would pass visible to FlatList as data.

React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

const shops = [
  { id: 1, name: "Rohtak Sweets", city: "Rohtak", rating: 4.2 },
  { id: 2, name: "Pune Pav Corner", city: "Pune", rating: 4.7 },
  { id: 3, name: "Kochi Fish Fry", city: "Kochi", rating: 4.5 },
  { id: 4, name: "Lucknow Kebab House", city: "Lucknow", rating: 4.8 },
  { id: 5, name: "Jaipur Lassi Wala", city: "Jaipur", rating: 4.6 },
];

function ShopList() {
  const [query, setQuery] = useState("");
  const [bestFirst, setBestFirst] = useState(false);

  const q = query.trim().toLowerCase();
  const filtered = shops.filter((s) => s.name.toLowerCase().includes(q) || s.city.toLowerCase().includes(q));
  const visible = bestFirst ? filtered.toSorted((a, b) => b.rating - a.rating) : filtered;

  return (
    <div style={{ fontFamily: "sans-serif" }}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search shop or city" />{" "}
      <button onClick={() => setBestFirst(!bestFirst)}>{bestFirst ? "Original order" : "Best rated first"}</button>
      {visible.length === 0 ? (
        <p>No shops match "{query}".</p>
      ) : (
        <ol>
          {visible.map((s) => <li key={s.id}>{s.name} ({s.city}) {s.rating}</li>)}
        </ol>
      )}
    </div>
  );
}

createRoot(document.getElementById("root")).render(<ShopList />);

Type "pune" and only Pune Pav Corner remains. Type "pu" and Jaipur Lassi Wala appears too, because "Jaipur" also contains "pu". Clear it and press "Best rated first": Lucknow Kebab House moves to the top. Note that toSorted returns a new array and does not change shops, which matters because React state must never be changed in place.

Step 3: pull to refresh and load more

FlatList has pull to refresh built in. Give it refreshing (a boolean) and onRefresh (a function).

For infinite scroll, onEndReached runs when the user scrolls close to the bottom. onEndReachedThreshold={0.5} means "when the end is half a screen away".

React JSX
import { useState } from "react";
import { FlatList, Text, ActivityIndicator } from "react-native";

function makePage(page) {
  return Array.from({ length: 20 }, (_, i) => ({ id: `${page}-${i}`, title: `Order #${page * 20 + i + 1}` }));
}

export default function Orders() {
  const [items, setItems] = useState(makePage(0));
  const [page, setPage] = useState(0);
  const [refreshing, setRefreshing] = useState(false);

  function loadMore() {
    if (page >= 4) return; // pretend the server has 5 pages
    const next = page + 1;
    setItems((old) => [...old, ...makePage(next)]);
    setPage(next);
  }

  function refresh() {
    setRefreshing(true);
    setItems(makePage(0));
    setPage(0);
    setRefreshing(false);
  }

  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.title}</Text>}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      refreshing={refreshing}
      onRefresh={refresh}
      ListFooterComponent={page < 4 ? <ActivityIndicator style={{ margin: 16 }} /> : null}
    />
  );
}

What you should see: 20 orders. As you scroll down, 20 more appear each time, until 100 orders are shown and the spinner at the bottom disappears. Pulling down resets the list to the first 20. In a real app, loadMore would fetch the next page from a server, which you will do in Fetching Data.

Step 4: grids and horizontal lists

Set numColumns={2} for a grid. Each row is then a row of items, which you can space with columnWrapperStyle. Set horizontal for a sideways list, like a row of offers.

React JSX
<FlatList
  data={products}
  keyExtractor={(p) => p.id}
  numColumns={2}
  columnWrapperStyle={{ gap: 12 }}
  contentContainerStyle={{ gap: 12, padding: 12 }}
  renderItem={({ item }) => (
    <View style={{ flex: 1, padding: 12, borderRadius: 10, backgroundColor: "#eef6fb" }}>
      <Text>{item.name}</Text>
      <Text>₹{item.price}</Text>
    </View>
  )}
/>

What you should see: product cards in two equal columns. flex: 1 on each card makes the two cards in a row share the width.

Common mistakes

  • Using map in a ScrollView for long lists. Use FlatList for anything long or growing.
  • Missing or duplicate keys. Use a stable, unique id. Avoid the array index if items can be added, removed or reordered.
  • Expecting renderItem to get the item directly. It gets { item, index }.
  • Putting a FlatList inside a vertical ScrollView. This breaks virtualisation and shows a warning. Use ListHeaderComponent for content above the list.
  • Calling onEndReached logic twice. Guard it with a check such as "already loading" or "no more pages".

Key points

  • FlatList is virtualised: it only renders rows near the screen.
  • data, renderItem and keyExtractor are the core props; renderItem receives { item, index }.
  • Use ListHeaderComponent, ListFooterComponent, ListEmptyComponent and ItemSeparatorComponent for list chrome.
  • refreshing with onRefresh gives pull to refresh; onEndReached gives infinite scroll.
  • numColumns makes grids; horizontal makes sideways lists.
  • Pass extraData when rows depend on state outside data.

Exercise

Build a "Class 8 students" list with 12 students (name and marks). Show a header "Results", sort the list by marks from highest to lowest, and highlight students with 90 or more marks in green. Add a search TextInput above the list that filters by name, and show "No student found" when nothing matches.

Show solution
React JSX
import { useState } from "react";
import { FlatList, Text, TextInput, View } from "react-native";

const students = [
  { id: "1", name: "Aarav", marks: 92 }, { id: "2", name: "Isha", marks: 88 },
  { id: "3", name: "Kabir", marks: 75 }, { id: "4", name: "Meera", marks: 95 },
  { id: "5", name: "Rohan", marks: 67 }, { id: "6", name: "Ananya", marks: 90 },
  { id: "7", name: "Farhan", marks: 81 }, { id: "8", name: "Priya", marks: 99 },
  { id: "9", name: "Dev", marks: 58 }, { id: "10", name: "Zoya", marks: 84 },
  { id: "11", name: "Om", marks: 71 }, { id: "12", name: "Tara", marks: 93 },
];

export default function Results() {
  const [query, setQuery] = useState("");
  const visible = students
    .filter((s) => s.name.toLowerCase().includes(query.trim().toLowerCase()))
    .sort((a, b) => b.marks - a.marks); // safe: filter already made a new array

  return (
    <View style={{ flex: 1 }}>
      <TextInput value={query} onChangeText={setQuery} placeholder="Search name" style={{ borderWidth: 1, margin: 16, padding: 10 }} />
      <FlatList
        data={visible}
        keyExtractor={(s) => s.id}
        ListHeaderComponent={<Text style={{ fontSize: 20, paddingHorizontal: 16 }}>Results</Text>}
        ListEmptyComponent={<Text style={{ padding: 16 }}>No student found</Text>}
        renderItem={({ item }) => (
          <Text style={{ padding: 16, backgroundColor: item.marks >= 90 ? "#d7f5e3" : "transparent" }}>
            {item.name}: {item.marks}
          </Text>
        )}
      />
    </View>
  );
}

The search box sits outside the list so it does not scroll away. The visible array is calculated during render from students and query, so it never goes out of sync.

Quick Quiz

1. Why is FlatList faster than ScrollView for long lists?

2. What does renderItem receive?

3. Which prop shows a message when data is empty?

4. Rows depend on a selectedId state that is not in data. What should you add?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call