React Native Basics: Build Your First Mobile App
Lists with FlatList
In this lesson you will
- Render a list with FlatList using data, renderItem and keyExtractor
- Add separators, a header, an empty message and pull to refresh
- Explain why FlatList is better than mapping an array inside a ScrollView for long lists
Introduction
Almost every app has a list: messages, products, songs, settings. In the last lessons you could show a few items with ScrollView. That stops working well when there are hundreds or thousands of rows. This lesson introduces FlatList, the standard tool for lists in React Native.
Why not just map inside a ScrollView?
On the web you might write items.map(...) and put the result in a page. In React Native, a ScrollView builds every child at once, even the ones far off screen. For 5 items that is fine. For 5,000 items the phone must create 5,000 native views up front, which is slow and uses a lot of memory.
FlatList is different. It renders only the rows that are on screen, plus a few near them, and recycles the rest as you scroll. The official docs say content is "rendered asynchronously offscreen" to "constrain memory and enable smooth scrolling."
Quick quiz
What is the main advantage of FlatList over a ScrollView with many children?
The three props you always need
React (TSX)
import { FlatList, StyleSheet, Text, View } from "react-native";
type Song = { id: string; title: string; artist: string };
const SONGS: Song[] = [
{ id: "1", title: "Blue in Green", artist: "Miles Davis" },
{ id: "2", title: "Clair de Lune", artist: "Claude Debussy" },
{ id: "3", title: "Take Five", artist: "The Dave Brubeck Quartet" },
];
export default function Playlist() {
return (
<FlatList
data={SONGS}
keyExtractor={(song) => song.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.artist}>{item.artist}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16 },
title: { fontSize: 18, fontWeight: "bold" },
artist: { color: "#666" },
});datais the array of items.renderItemis a function that receives an object withitem(one entry from the array) and returns the row to draw.keyExtractorreturns a unique string for each item. React uses it to track rows as the list changes. By default FlatList looks foritem.key, thenitem.id, and finally falls back to the position, so giving each item anidoften works without it. Writing it explicitly is clearer.
Separators, headers, footers and empty lists
These extra props make a list feel finished:
React (TSX)
import { FlatList, StyleSheet, Text, View } from "react-native";
type Task = { id: string; label: string };
export default function Tasks({ tasks }: { tasks: Task[] }) {
return (
<FlatList
data={tasks}
keyExtractor={(task) => task.id}
renderItem={({ item }) => <Text style={styles.row}>{item.label}</Text>}
ItemSeparatorComponent={() => <View style={styles.line} />}
ListHeaderComponent={<Text style={styles.heading}>Today</Text>}
ListEmptyComponent={<Text style={styles.empty}>Nothing to do. Enjoy your day!</Text>}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16, fontSize: 16 },
line: { height: StyleSheet.hairlineWidth, backgroundColor: "#ccc" },
heading: { fontSize: 24, fontWeight: "bold", padding: 16 },
empty: { padding: 16, color: "#666" },
});ItemSeparatorComponentdraws something between rows, not above the first or below the last.ListHeaderComponentandListFooterComponentscroll with the list.ListEmptyComponentshows whendatais empty. Always give your lists an empty state.
Pull to refresh
Add two props and the list gets the standard pull-down-to-refresh gesture:
React (TSX)
import { useState } from "react";
import { FlatList, Text } from "react-native";
export default function News() {
const [items, setItems] = useState(["First story", "Second story"]);
const [refreshing, setRefreshing] = useState(false);
function reload() {
setRefreshing(true);
setTimeout(() => {
setItems((old) => [`Story ${old.length + 1}`, ...old]);
setRefreshing(false);
}, 800);
}
return (
<FlatList
data={items}
keyExtractor={(item) => item}
renderItem={({ item }) => <Text style={{ padding: 16 }}>{item}</Text>}
refreshing={refreshing}
onRefresh={reload}
/>
);
}onRefresh runs when the user pulls down. You must set refreshing to true while you load and back to false when done, so the spinner shows. The timer stands in for a real network request, which a later lesson covers.
Updating a list that is changing
FlatList is a pure component: it only redraws when its props change. If a row depends on something outside data, such as which item is selected, pass it as extraData:
React (TSX)
import { useState } from "react";
import { FlatList, Pressable, Text } from "react-native";
const COLOURS = ["Red", "Green", "Blue"];
export default function Picker() {
const [selected, setSelected] = useState("Green");
return (
<FlatList
data={COLOURS}
extraData={selected}
keyExtractor={(name) => name}
renderItem={({ item }) => (
<Pressable onPress={() => setSelected(item)}>
<Text style={{ padding: 16, fontWeight: item === selected ? "bold" : "normal" }}>{item}</Text>
</Pressable>
)}
/>
);
}Without extraData, changing selected would not redraw the rows, because data itself did not change. Remember also that FlatList does not keep row state once a row scrolls far off screen, so keep important data in your own state.
Quick quiz
A row's style depends on a selected value held in state, but the data array did not change. What should you pass to FlatList?
Two columns and sections
numColumns={2}makes a grid. Items fill each row, and the number cannot change while the list is on screen unless you also change the list'skey.- When data has groups, such as contacts by letter, use
SectionList, a sibling component with the same ideas.
Try it yourself
Make a contacts list from an array of 30 names. Add a header, an empty message (try an empty array), and a Pressable row that logs the tapped name. Then add a TextInput above the list and filter the data as the user types.
Summary
- Use
FlatListfor any list that could be long. It renders only what is on screen, unlike a ScrollView that builds everything. - You need
data,renderItemand a unique key throughkeyExtractor, and you can add separators, a header, an empty state and pull to refresh. - Pass anything a row depends on, besides
data, asextraData.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading