Skip to Content

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 les key du 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.