Lists with FlatList
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.
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.
Step 2: header, footer and empty state
Real lists need a title at the top, something at the bottom, and a friendly message when there is nothing to show.
<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.
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".
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.
<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
mapin aScrollViewfor long lists. UseFlatListfor 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
renderItemto get the item directly. It gets{ item, index }. - Putting a
FlatListinside a verticalScrollView. This breaks virtualisation and shows a warning. UseListHeaderComponentfor content above the list. - Calling
onEndReachedlogic twice. Guard it with a check such as "already loading" or "no more pages".
Key points
FlatListis virtualised: it only renders rows near the screen.data,renderItemandkeyExtractorare the core props;renderItemreceives{ item, index }.- Use
ListHeaderComponent,ListFooterComponent,ListEmptyComponentandItemSeparatorComponentfor list chrome. refreshingwithonRefreshgives pull to refresh;onEndReachedgives infinite scroll.numColumnsmakes grids;horizontalmakes sideways lists.- Pass
extraDatawhen rows depend on state outsidedata.
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
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?
Virtualisation keeps the number of rendered rows small, however long the data is.
2. What does renderItem receive?
Destructure it as ({ item, index }).
3. Which prop shows a message when data is empty?
ListEmptyComponent renders only when the list has no items.
4. Rows depend on a selectedId state that is not in data. What should you add?
extraData tells FlatList to re-render rows when that value changes.
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
