FlatList
How FlatList turns an array into a scrolling list, and why it only keeps the rows near the screen mounted.
<FlatList data={items} renderItem={({ item }) => <Row item={item} />} keyExtractor={(item) => item.id} />FlatList draws a scrolling list from an array. It exists because mounting a thousand rows at once is slow and uses a lot of memory. FlatList keeps only the rows near the viewport mounted and unmounts the rest as you scroll. That technique is called virtualisation, and it is the whole reason to prefer FlatList over a ScrollView with a .map() inside it.
The three props that matter
data is the array. renderItem is called for each row that needs to exist and returns the row's elements. keyExtractor returns a stable string id for an item, so React can match a row to its data between renders.
renderItem is passed one object, not the item. The object is { item, index, separators }. The common form destructures it in the parameter list. These two are the same function:
renderItem={({ item }) => <Row track={item} />}
renderItem={(info) => <Row track={info.item} />}
If you leave keyExtractor out, FlatList looks for item.key, then item.id, then falls back to the index.
What trips people up
This is not an HTML list. Rows do not inherit styles from the list, so each row carries its own, and any string must sit inside a Text.
Because far-off rows are unmounted, state held inside a row is lost when it scrolls away and comes back. Keep per-row state in the data, not in the row component.
Finally, keyExtractor must return a string: use String(item.id) for numeric ids.
Example
import { FlatList, StyleSheet, Text, View } from "react-native";
type Track = { id: string; title: string; artist: string };
const TRACKS: Track[] = [
{ id: "1", title: "Pulse", artist: "Noam" },
{ id: "2", title: "Low Tide", artist: "Vela" },
{ id: "3", title: "Grain", artist: "Ouro" },
];
type RowProps = { track: Track };
function Row({ track }: RowProps) {
return (
<View style={styles.row}>
<Text style={styles.title}>{track.title}</Text>
<Text style={styles.artist}>{track.artist}</Text>
</View>
);
}
export default function TrackList() {
return (
<FlatList
data={TRACKS}
// renderItem is handed one object; pull item out of it
renderItem={({ item }) => <Row track={item} />}
// must return a string, so convert numeric ids with String()
keyExtractor={(item) => item.id}
// padding goes on the scrolling content, not on the list itself
contentContainerStyle={styles.list}
/>
);
}
const styles = StyleSheet.create({
list: { padding: 16 },
row: { paddingVertical: 12 },
title: { fontSize: 16, fontWeight: "600" },
artist: { fontSize: 13, color: "#666" },
});When to use it
- Render a contacts or inbox screen whose length comes from data rather than from fixed markup.
- Replace a ScrollView full of .map() output that stutters once the array grows past a few dozen items.
- Lay out a grid of image tiles with numColumns instead of calculating widths by hand.
- Show a sideways shelf of cards at the top of a home screen.
More examples
A horizontal row of cards
On screen: a single row of fixed-width cards that scrolls sideways, with 16pt of padding at each end. Note that `horizontal` is the bare-attribute shorthand for `horizontal={true}` - the same prop either way. Reach for this for a shelf of covers or a chip bar; the virtualisation works the same sideways as it does vertically.
import { FlatList, StyleSheet, Text, View } from "react-native";
type Album = { id: string; name: string };
type AlbumStripProps = { albums: Album[] };
export default function AlbumStrip({ albums }: AlbumStripProps) {
return (
<FlatList
data={albums}
horizontal
showsHorizontalScrollIndicator={false}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.name}>{item.name}</Text>
</View>
)}
contentContainerStyle={styles.strip}
/>
);
}
const styles = StyleSheet.create({
strip: { paddingHorizontal: 16, gap: 12 },
card: { width: 140, height: 140, justifyContent: "flex-end", padding: 12, backgroundColor: "#eee", borderRadius: 12 },
name: { fontSize: 14, fontWeight: "600" },
});A two-column grid
On screen: square cells two to a row, each filling half the width minus the gap. `numColumns` works only with a vertical list - setting it above 1 together with `horizontal` is not silently ignored, it throws an invariant error ("numColumns does not support horizontal."). Give cells `flex: 1` rather than a percentage width, and use `columnWrapperStyle` for the gap between the two cells in a row - passing `columnWrapperStyle` to a single-column list throws in the same way.
import { FlatList, StyleSheet, Text, View } from "react-native";
type Photo = { id: string; label: string };
type PhotoGridProps = { photos: Photo[] };
export default function PhotoGrid({ photos }: PhotoGridProps) {
return (
<FlatList
data={photos}
numColumns={2} // only valid when the list is vertical
columnWrapperStyle={styles.row} // styles each row of two cells
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.cell}>
<Text>{item.label}</Text>
</View>
)}
contentContainerStyle={styles.list}
/>
);
}
const styles = StyleSheet.create({
list: { padding: 8 },
row: { gap: 8 },
// flex: 1 makes each cell take half the row; there is no CSS width: 50% + margin maths here
cell: { flex: 1, aspectRatio: 1, backgroundColor: "#eee", borderRadius: 8, padding: 8 },
});Replacing a mapped ScrollView
Both render the same screen, and that is the problem - the slow one looks correct until the data grows. Use the ScrollView form only for a short, bounded set such as a settings page. Any list whose length comes from data belongs in a FlatList.
import { FlatList, ScrollView, StyleSheet, Text } from "react-native";
type Message = { id: string; body: string };
type InboxProps = { messages: Message[] };
// Mounts every row immediately. Fine for ten items, a freeze at a thousand.
export function SlowInbox({ messages }: InboxProps) {
return (
<ScrollView>
{messages.map((m) => (
<Text key={m.id} style={styles.row}>
{m.body}
</Text>
))}
</ScrollView>
);
}
// Mounts roughly a screenful, then more as you scroll.
export default function Inbox({ messages }: InboxProps) {
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text style={styles.row}>{item.body}</Text>}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16, fontSize: 16 },
});
Discussion