Conceptos básicos de listas
10 ejemplos para comenzar con Listas y Scroll - 7 básicos e intermedios y 3 avanzados.
Busca en todas las páginas de la documentación
10 ejemplos para comenzar con Listas y Scroll - 7 básicos e intermedios y 3 avanzados.
Cada ejemplo a continuación usa primitivos de listas nativos de React Native - sin paquetes adicionales. Scaffold una app estándar de Expo SDK 57 TypeScript y reemplaza App.tsx para ejecutar cada fragmento.
npx create-expo-app@latest MyListsApp --template blank-typescript
cd MyListsApp
npx expo startHerramientas: Estos ejemplos están dirigidos a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, y React 19.2.3.
ScrollView monta todos los hijos inmediatamente. Eso es aceptable cuando el número de elementos es pequeño y fijo - pantallas de configuración, texto legal, pasos de incorporación.
import { ScrollView, StyleSheet, Text, View } from "react-native";
const SETTINGS = [
{ id: "profile", label: "Edit profile" },
{ id: "notifications", label: "Notifications" },
{ id: "privacy", label: "Privacy" },
{ id: "help", label: "Help center" },
] as const;
export default function App() {
return (
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.heading}>Settings</Text>
{SETTINGS.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={styles.label}>{item.label}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
content: { padding: 16, gap: 8 },
heading: { fontSize: 22, fontWeight: "700", marginBottom: 8 },
row: {
padding: 16,
backgroundColor: "#f3f4f6",
borderRadius: 10,
},
label: { fontSize: 16 },
});ScrollView cuando tienes aproximadamente menos de 20 filas simples y la lista raramente crece en tiempo de ejecucióncontentContainerStyle rellena el contenido desplazable; la vista desplazable en sí aún necesita una altura limitada (flex: 1 en una View padre si es necesario)key - aquí item.id es estableFlatList - montar cientos de filas en una ScrollView ralentiza el hilo JS y consume mucha memoriaRelacionado: Virtualization Gotchas - por qué las listas
ScrollViewsin límites congela los dispositivos de gama baja
FlatList virtualiza filas: renderiza solo lo que es visible más un pequeño búfer. Pasa data y renderItem - ese es el mínimo viable para una lista.
import { FlatList, StyleSheet, Text, View } from "react-native";
type Post = { id: string; title: string };
const POSTS: Post[] = Array.from({ length: 200 }, (_, index) => ({
id: String(index + 1),
title: `Post #${index + 1}`,
}));
export default function App() {
return (
<FlatList
data={POSTS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.title}>{item.title}</Text>
</View>
)}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
row: {
padding: 16,
marginBottom: 8,
backgroundColor: "#eef2ff",
borderRadius: 10,
},
title: { fontSize: 16, fontWeight: "600" },
});data es una matriz simple - cuando cambia, FlatList difiere por keyExtractor y recicla vistas nativasrenderItem recibe { item, index, separators } - mantenla pura y rápida; el trabajo pesado pertenece a componentes hijo memoizadosView style={{ flex: 1 }} dentro de una pantalla con un encabezadoFlatList es obligatorioRelacionado: FlatList Recipes - separadores,
getItemLayout, y pulido de estado vacío
keyExtractor le dice a React Native cómo hacer coincidir filas en actualizaciones. Las keys inestables causan parpadeo, pérdida de posición de desplazamiento y estado de selección roto.
import { FlatList, StyleSheet, Text, View } from "react-native";
type Task = { id: string; label: string; done: boolean };
const TASKS: Task[] = [
{ id: "t1", label: "Ship list empty state", done: false },
{ id: "t2", label: "Add pull-to-refresh", done: true },
{ id: "t3", label: "Profile SectionList", done: false },
];
export default function App() {
return (
<FlatList
data={TASKS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.bullet}>{item.done ? "✓" : "○"}</Text>
<Text style={[styles.label, item.done && styles.done]}>{item.label}</Text>
</View>
)}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
row: { flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12 },
bullet: { fontSize: 18, width: 24, textAlign: "center" },
label: { fontSize: 16, flex: 1 },
done: { textDecorationLine: "line-through", color: "#9ca3af" },
});id emitido por servidor sobre índice de matriz - reordenar, filtrar o anteponer filas rompe las keys basadas en índiceMath.random() o Date.now() por renderizado - las keys deben ser estables durante la vida útil de la filakeyExtractor={(item) => String(item.id)}Relacionado: FlatList Recipes -
getItemLayoutse empareja bien con ids estables para scroll-to-index
Cuando data es [], FlatList no renderiza nada a menos que proporciones ListEmptyComponent - el hook estándar para loading, error y estados de cero resultados.
import { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from "react-native";
type Bookmark = { id: string; title: string };
export default function App() {
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setBookmarks([]);
setLoading(false);
}, 1200);
return () => clearTimeout(timer);
}, []);
return (
<FlatList
data={bookmarks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text>{item.title}</Text>
</View>
)}
ListEmptyComponent={
loading ? (
<ActivityIndicator style={styles.empty} />
) : (
<View style={styles.empty}>
<Text style={styles.emptyTitle}>No bookmarks yet</Text>
<Text style={styles.emptyBody}>
Save articles from the feed and they will show up here.
</Text>
</View>
)
}
contentContainerStyle={bookmarks.length === 0 ? styles.emptyContainer : styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
emptyContainer: { flexGrow: 1, justifyContent: "center" },
empty: { alignItems: "center", padding: 32 },
emptyTitle: { fontSize: 18, fontWeight: "600", marginBottom: 8 },
emptyBody: { fontSize: 14, color: "#6b7280", textAlign: "center", lineHeight: 20 },
row: { padding: 16, backgroundColor: "#f9fafb", borderRadius: 8, marginBottom: 8 },
});ListEmptyComponent renderiza en lugar de renderItem cuando data.length === 0 - un componente puede ramificarse en loading vs vacíoflexGrow: 1 a contentContainerStyle cuando el estado vacío debe centrar verticalmente dentro de la pantallaListHeaderComponent - los usuarios aún ven un cuerpo de lista vacío debajoRelacionado: FlatList Recipes - patrones de diseño de estado vacío y filas de esqueleto
Los usuarios móviles esperan refreshing en overscroll. Conecta el estado refreshing a onRefresh y pasa un RefreshControl - o usa los props atajo de FlatList.
import { useCallback, useState } from "react";
import { FlatList, RefreshControl, StyleSheet, Text, View } from "react-native";
type Message = { id: string; body: string };
const INITIAL: Message[] = [
{ id: "1", body: "Welcome to the inbox." },
{ id: "2", body: "Pull down to fetch newer messages." },
];
export default function App() {
const [messages, setMessages] = useState(INITIAL);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await new Promise((resolve) => setTimeout(resolve, 900));
setMessages((prev) => [
{ id: String(Date.now()), body: "New message just arrived." },
...prev,
]);
setRefreshing(false);
}, []);
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.body}>{item.body}</Text>
</View>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
row: {
padding: 16,
marginBottom: 8,
backgroundColor: "#ecfdf5",
borderRadius: 10,
},
body: { fontSize: 15, lineHeight: 22 },
});refreshing debe volverse true cuando el trabajo comienza y false cuando termina - dejarlo atascado muestra un spinner perpetuoonRefresh si los usuarios presionan repetidamente; ignora llamadas superpuestas con una ref o retorno tempranocolors (Android) y tintColor (iOS) en RefreshControl para coincidir con la marcaonEndReached, no pull-to-refresh - ver la guía de infinite-scroll para patrones load-moreRelacionado: Infinite Scroll & Pagination -
onEndReached, cursores y protecciones contra páginas duplicadas
Los separadores pertenecen a props de lista, no dentro de cada fila. ItemSeparatorComponent mantiene renderItem enfocado en el contenido de fila.
import { FlatList, StyleSheet, Text, View } from "react-native";
type City = { id: string; name: string; country: string };
const CITIES: City[] = [
{ id: "1", name: "Tokyo", country: "Japan" },
{ id: "2", name: "Berlin", country: "Germany" },
{ id: "3", name: "São Paulo", country: "Brazil" },
{ id: "4", name: "Toronto", country: "Canada" },
];
function Separator() {
return <View style={styles.separator} />;
}
export default function App() {
return (
<FlatList
data={CITIES}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.country}>{item.country}</Text>
</View>
)}
ItemSeparatorComponent={Separator}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16, backgroundColor: "#fff", borderRadius: 12 },
row: { paddingVertical: 14 },
name: { fontSize: 16, fontWeight: "600" },
country: { fontSize: 13, color: "#6b7280", marginTop: 2 },
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: "#e5e7eb",
},
});renderItemFlatList también expone ItemSeparatorComponent vía renderItem de separators.highlight() para retroalimentación táctil - útil en selectores de contactosSectionList con renderSectionHeader en lugar de fingir divisores en un array planoReact.memo si asignan degradados o SVG en cada reciclajeRelacionado: FlatList Recipes - separadores insertados y excepciones de borde de última fila
Cuando las filas pertenecen a grupos nombrados - contactos por letra, tareas por estado - SectionList renderiza secciones con encabezados sticky opcionales.
import { SectionList, StyleSheet, Text, View } from "react-native";
type Person = { id: string; name: string };
type Section = { title: string; data: Person[] };
const SECTIONS: Section[] = [
{
title: "Engineering",
data: [
{ id: "1", name: "Alex Kim" },
{ id: "2", name: "Jordan Lee" },
],
},
{
title: "Design",
data: [
{ id: "3", name: "Sam Rivera" },
{ id: "4", name: "Casey Ng" },
],
},
];
export default function App() {
return (
<SectionList
sections={SECTIONS}
keyExtractor={(item) => item.id}
renderSectionHeader={({ section }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{section.title}</Text>
</View>
)}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.name}>{item.name}</Text>
</View>
)}
stickySectionHeadersEnabled
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { paddingBottom: 24 },
header: {
backgroundColor: "#f3f4f6",
paddingHorizontal: 16,
paddingVertical: 8,
},
headerText: { fontSize: 13, fontWeight: "700", color: "#4b5563" },
row: {
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: "#fff",
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "#e5e7eb",
},
name: { fontSize: 16 },
});{ title, data: T[] } - data sostiene las filas de ese grupo; otros metadatos (conteos, insignias) pueden vivir en el objeto de secciónrenderSectionHeader recibe { section } - estila encabezados de manera diferente a las filas para capacidad de exploraciónstickySectionHeadersEnabled mantiene la etiqueta de sección actual fija mientras se desplaza - estándar para contactos y calendariossectionsRelacionado: SectionList & Grouped Data - footers, secciones multi-columna y ajuste de encabezado sticky
El chrome de pantalla que se desplaza con la lista - títulos, filtros, pistas load-more - pertenece a ranuras de encabezado/pie, no dentro de cada fila.
import { FlatList, StyleSheet, Text, View } from "react-native";
type Item = { id: string; label: string };
const DATA: Item[] = [
{ id: "1", label: "Audit list keys" },
{ id: "2", label: "Measure scroll jank" },
{ id: "3", label: "Ship skeleton loader" },
];
function ListHeader() {
return (
<View style={styles.header}>
<Text style={styles.title}>Sprint backlog</Text>
<Text style={styles.subtitle}>3 items · swipe down on a feed to refresh</Text>
</View>
);
}
function ListFooter() {
return (
<Text style={styles.footer}>End of backlog - footer scrolls with content.</Text>
);
}
export default function App() {
return (
<FlatList
data={DATA}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text>{item.label}</Text>
</View>
)}
ListHeaderComponent={ListHeader}
ListFooterComponent={ListFooter}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
header: { marginBottom: 16 },
title: { fontSize: 22, fontWeight: "700" },
subtitle: { fontSize: 14, color: "#6b7280", marginTop: 4 },
row: {
padding: 14,
backgroundColor: "#f9fafb",
borderRadius: 8,
marginBottom: 8,
},
footer: {
textAlign: "center",
color: "#9ca3af",
fontSize: 13,
marginTop: 8,
marginBottom: 24,
},
});ListFooterComponent es el lugar idiomático para spinners de "loading more" cuando se pagina (no ListEmptyComponent)View fijo en la parte superior, FlatList con flex: 1 abajoRelacionado: Infinite Scroll & Pagination - loaders de pie de página y
onEndReachedThreshold
Carruseles, chips de etiqueta y tiras de miniaturas usan la misma API de FlatList con horizontal - todavía virtualizado a lo largo del eje cruzado.
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { useState } from "react";
const TAGS = [
{ id: "all", label: "All" },
{ id: "design", label: "Design" },
{ id: "mobile", label: "Mobile" },
{ id: "perf", label: "Performance" },
{ id: "a11y", label: "Accessibility" },
] as const;
export default function App() {
const [active, setActive] = useState<string>("all");
return (
<View style={styles.screen}>
<Text style={styles.heading}>Topics</Text>
<FlatList
data={TAGS}
horizontal
showsHorizontalScrollIndicator={false}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
const selected = item.id === active;
return (
<Pressable
onPress={() => setActive(item.id)}
style={[styles.chip, selected && styles.chipSelected]}
>
<Text style={[styles.chipText, selected && styles.chipTextSelected]}>
{item.label}
</Text>
</Pressable>
);
}}
contentContainerStyle={styles.strip}
/>
<Text style={styles.caption}>Selected: {active}</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, paddingTop: 24 },
heading: { fontSize: 18, fontWeight: "600", paddingHorizontal: 16, marginBottom: 12 },
strip: { paddingHorizontal: 16, gap: 8 },
chip: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 999,
backgroundColor: "#e5e7eb",
},
chipSelected: { backgroundColor: "#2563eb" },
chipText: { fontSize: 14, color: "#374151" },
chipTextSelected: { color: "#fff", fontWeight: "600" },
caption: { padding: 16, color: "#6b7280" },
});horizontal invierte el eje de desplazamiento - keyExtractor y renderItem se mantienen igualshowsHorizontalScrollIndicator={false} para rieles de chips; mantenlo true para carruseles de imágenes donde la affordance ayudagap en contentContainerStyle (RN 0.71+) o margen en elementos para espaciado - evita hacks de relleno en el último chipFlatList vertical necesitan setup de gestos cuidadoso - ver la guía de scrollables anidadas antes de enviar feeds con carruseles en líneaRelacionado: Nested Scrollables - conflictos de gestos vertical + horizontal
FlatList memoiza filas contra data. Cuando la apariencia de fila depende de estado externo (selección, tema, id expandido), pasa extraData para que las filas visibles se actualicen.
import { useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
type File = { id: string; name: string };
const FILES: File[] = [
{ id: "a", name: "Quarterly-report.pdf" },
{ id: "b", name: "Launch-checklist.md" },
{ id: "c", name: "App-icons.sketch" },
];
export default function App() {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<FlatList
data={FILES}
extraData={selectedId}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
const selected = item.id === selectedId;
return (
<Pressable
onPress={() => setSelectedId(item.id)}
style={[styles.row, selected && styles.rowSelected]}
>
<Text style={[styles.name, selected && styles.nameSelected]}>
{item.name}
</Text>
</Pressable>
);
}}
contentContainerStyle={styles.content}
/>
);
}
const styles = StyleSheet.create({
content: { padding: 16 },
row: {
padding: 16,
borderRadius: 10,
backgroundColor: "#f9fafb",
marginBottom: 8,
borderWidth: 2,
borderColor: "transparent",
},
rowSelected: { borderColor: "#2563eb", backgroundColor: "#eff6ff" },
name: { fontSize: 15 },
nameSelected: { fontWeight: "600", color: "#1d4ed8" },
});extraData, alternar selectedId puede no re-renderizar filas visibles porque la referencia del array data no ha cambiadoselectedId), un objeto o un array - lo que sea que tu renderItem lea desde fuera de itemSet o Record en state y pásalo como extraData, o incrusta flags directamente en data cuando controlas el modeloReact.memo en componentes de fila extraídos y un callback renderItem estable para listas más grandes - ver ajuste de rendimiento para windowSize y maxToRenderPerBatchRelacionado: List Performance Tuning -
windowSize, filas memoizadas, y tamaños de lote | FlashList vs FlatList - cuándo actualizar librerías de virtualización
Versiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 19 jul 2026