Chapitre 3.4 — FlatList : la virtualisation obligatoire
Où on en est : vous savez afficher et toucher. Reste à lister — et c’est LE chapitre où le réflexe web (
items.map()) doit mourir.
⏱️ TL;DR — Sur mobile, une liste de 500 items rendus d’un coup = app qui rame ou crash mémoire. FlatList ne rend que les items visibles (+ une marge) et recycle le reste : c’est la virtualisation. API :
data,renderItem,keyExtractor, plus l’écosystème (séparateurs, header, empty state, pull-to-refresh, onEndReached). Règles de perf : items mémoïsés, pas de fonctions inline recréées, hauteurs stables.
🎯 Objectifs
- Comprendre pourquoi
.map()est interdit pour les listes longues (et OK pour 4 réponses). - Maîtriser l’API FlatList complète : data, renderItem, keyExtractor et les props d’habillage.
- Connaître les 4 règles de performance qui font une liste fluide.
- Construire la liste de l’historique des sessions de Drill.
Le crime du .map()
Sur le web, ceci est idiomatique :
// ❌ Réflexe web — mortel en RN pour une longue liste
<ScrollView>
{sessions.map(s => <SessionRow key={s.id} session={s} />)}
</ScrollView>Le DOM du navigateur encaisse des milliers de nœuds. En RN, chaque item = de vrais widgets natifs : 500 sessions × ~10 vues = 5 000 objets natifs créés, mesurés, dessinés d’un coup, hors écran pour 95 % d’entre eux. Résultat : freeze au montage, mémoire engloutie.
La virtualisation inverse la logique : ne créer QUE ce qui est visible, recycler en scrollant.
La limite du bon sens : .map() reste parfait pour un nombre petit et borné d’éléments —
les 4 AnswerButton d’une question Drill, par exemple. La règle : borné et petit → map ;
non borné (données) → FlatList.
L’API essentielle
L’historique des sessions de Drill :
// app/history.tsx — la liste de l'historique (données en dur pour l'instant)
import { FlatList, StyleSheet, Text, View } from 'react-native'
type Session = {
id: string
theme: string
score: number
total: number
playedAt: string
}
function SessionRow({ session }: { session: Session }) {
return (
<View style={styles.row}>
<Text style={styles.theme}>{session.theme}</Text>
<Text style={styles.score}>{session.score} pts — {session.total} questions</Text>
</View>
)
}
export default function HistoryScreen() {
return (
<FlatList
data={sessions}
keyExtractor={s => s.id}
renderItem={({ item }) => <SessionRow session={item} />}
ItemSeparatorComponent={() => <View style={styles.sep} />}
ListHeaderComponent={<Text style={styles.h1}>Historique</Text>}
ListEmptyComponent={
<Text style={styles.empty}>Aucune session — lance ton premier quiz !</Text>
}
contentContainerStyle={styles.content}
/>
)
}Le trio obligatoire :
data: le tableau (et rien que lui — les transformations se font avant).renderItem:({ item, index }) => JSX— le gabarit d’un item.keyExtractor: l’identité stable de chaque item. Comme leskeydu web, en plus critique : la virtualisation recycle sur cette base. Jamais l’index si la liste bouge.
Et l’habillage, tout déclaratif : ItemSeparatorComponent (filet entre items),
ListHeaderComponent / ListFooterComponent (contenu qui scrolle avec la liste),
ListEmptyComponent (état vide — soignez-le, c’est le premier écran que verra un nouvel
utilisateur), contentContainerStyle (le padding INTERNE du scroll — piège classique : style
stylerait le cadre, pas le contenu).
Interactions de liste
<FlatList
// Pull-to-refresh (re-télécharger les questions, Partie 7)
refreshing={isRefreshing}
onRefresh={reload}
// Scroll infini (charger la page suivante à 50 % de la fin)
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>Deux patterns servis « gratuitement » : le pull-to-refresh (geste universel mobile — Drill
s’en servira pour rafraîchir les banques de questions) et le scroll infini (onEndReached).
Les 4 règles de performance
Une FlatList lente, c’est presque toujours l’une de ces violations :
1. Extraire et mémoïser le composant d’item.
// ❌ Recrée l'arbre à chaque render du parent
renderItem={({ item }) => <View>…20 lignes inline…</View>}
// ✅ Composant nommé + React.memo : un item ne re-render que si SES props changent
const SessionRow = React.memo(function SessionRow({ session }: Props) { … })2. Des identités stables. keyExtractor stable, et attention aux props recréées à chaque
render du parent (onPress={() => …} inline passe une nouvelle fonction à chaque fois →
casse le memo ; useCallback au niveau du parent — détails en Partie 6).
3. Des hauteurs prévisibles. Si tous les items ont la même hauteur, getItemLayout épargne
la mesure ; sinon, éviter les items dont la hauteur « saute » au chargement (images sans
dimensions — chapitre 3.2 !).
4. Des items légers. Pas de logique lourde dans le render d’un item ; les calculs se font sur les données en amont.
⚡ Depuis SDK 5x — Pour les listes très exigeantes (milliers d’items hétérogènes, feed), l’écosystème a un cran au-dessus : FlashList (Shopify), API quasi identique, recyclage plus agressif. Réflexe 2026 : FlatList d’abord ; si le profiler (Partie 14) montre des saccades de liste, FlashList est un remplacement à ~5 lignes près. Drill n’en aura pas besoin — nos listes sont courtes.
💡 Pour un dev Next.js — FlatList ≈
react-window/TanStack Virtual, mais obligatoire et intégré. Sur le web, la virtualisation est une optimisation de cas extrême ; en RN, c’est le mode par défaut de TOUTE liste de données. Le.map()y est l’exception, pas la règle.
✏️ Exercices
1. Une liste de 40 thèmes s’affiche via .map() dans une ScrollView, sans problème visible.
Faut-il la migrer vers FlatList ? Argumentez les deux positions, puis tranchez.
✅ Solution
Pour le map : 40 items légers, ça passe. Pour FlatList : la liste est non bornée (des thèmes s’ajouteront), FlatList apporte gratuitement empty state, pull-to-refresh et separators, et le coût de migration est nul aujourd’hui vs douloureux plus tard. Verdict : FlatList — la règle « données → FlatList » évite d’avoir à surveiller la croissance. Le map reste pour les structures fixes (4 réponses, 3 stats).
2. Qu’affiche contentContainerStyle={{ padding: 16 }} que style={{ padding: 16 }} ne
donnerait pas ? Testez mentalement avec une liste plus haute que l’écran.
✅ Solution
style pose le padding sur le cadre de la liste (la fenêtre de scroll rétrécit — et le
contenu scrolle sous le padding, coupé aux bords). contentContainerStyle pose le padding
dans le contenu scrollable : premier item espacé en haut, dernier item espacé en bas, et le
scroll traverse naturellement. Pour du padding de liste, c’est presque toujours
contentContainerStyle qu’on veut.
3. Le lint hurle : votre renderItem inline contient un onPress={() => open(item.id)}.
Réécrivez le pattern proprement (composant mémoïsé + callback stable).
✅ Solution
const SessionRow = React.memo(function SessionRow({ session, onOpen }: {
session: Session
onOpen: (id: string) => void
}) {
return (
<Pressable onPress={() => onOpen(session.id)}>…</Pressable>
)
})
// Dans l'écran :
const handleOpen = useCallback((id: string) => router.push(`/session/${id}`), [])
<FlatList renderItem={({ item }) => <SessionRow session={item} onOpen={handleOpen} />} … />handleOpen est stable (useCallback) → SessionRow mémoïsé ne re-render pas quand le parent
re-render. L’arrow interne à SessionRow est locale à l’item : elle ne casse rien.
🧠 Quiz
1. Pourquoi .map() dans une ScrollView est-il dangereux pour 500 items ?
Réponse
Tous les items deviennent des widgets natifs réels, créés/mesurés/dessinés d’un coup, y compris hors écran → freeze au montage et surconsommation mémoire. FlatList ne rend que le visible et recycle.
2. Quel est le trio de props minimal d’une FlatList ?
Réponse
data (le tableau), renderItem (le gabarit), keyExtractor (l’identité stable de chaque
item).
3. Comment ajouter un pull-to-refresh ?
Réponse
Props refreshing (booléen d’état) + onRefresh (callback) — le spinner natif est géré.
4. Pourquoi mémoïser le composant d’item avec React.memo ?
Réponse
Pour qu’un item ne re-render que si ses props changent — sinon chaque render du parent re-rend tous les items visibles. Condition : lui passer des props stables (useCallback pour les fonctions).
5. Quand considérer FlashList ?
Réponse
Quand une FlatList profile mal malgré les règles de perf : très longues listes, items hétérogènes, feed complexe. API quasi identique, recyclage plus agressif (Shopify).
👉 Chapitre suivant : 3.5 — TextInput & le clavier — le clavier virtuel, ce voisin envahissant.