Recetas de FlatList
keyExtractor, getItemLayout, separadores, y estados vacíos - los props que usas en todas las pantallas de feed.
Busca en todas las páginas de la documentación
keyExtractor, getItemLayout, separadores, y estados vacíos - los props que usas en todas las pantallas de feed.
Tarjeta de referencia rápida - lista para copiar y pegar.
import { FlatList, StyleSheet, Text, View } from "react-native";
interface Message {
id: string;
body: string;
}
const ROW_HEIGHT = 72;
export function MessageList({ messages }: { messages: Message[] }) {
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text numberOfLines={2}>{item.body}</Text>
</View>
)}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
<View style={styles.empty}>
<Text style={styles.emptyTitle}>Sin mensajes aún</Text>
<Text style={styles.emptyBody}>Tira para actualizar o inicia una conversación.</Text>
</View>
}
contentContainerStyle={messages.length === 0 ? styles.emptyContainer : styles.list}
/>
);
}
const styles = StyleSheet.create({
list: { paddingHorizontal: 16, paddingBottom: 24 },
row: { height: ROW_HEIGHT, justifyContent: "center" },
separator: { height: StyleSheet.hairlineWidth, backgroundColor: "#e2e8f0" },
emptyContainer: { flexGrow: 1, justifyContent: "center", padding: 32 },
empty: { alignItems: "center", gap: 8 },
emptyTitle: { fontSize: 18, fontWeight: "700", color: "#0f172a" },
emptyBody: { fontSize: 14, color: "#64748b", textAlign: "center" },
});Cuándo usarlo: Cualquier lista vertical homogénea de desplazamiento - filas de bandeja de entrada, elementos de configuración, tarjetas de producto con altura uniforme.
import { memo, useCallback, useMemo, useState } from "react";
import {
FlatList,
Pressable,
StyleSheet,
Text,
View,
type ListRenderItem,
} from "react-native";
interface Contact {
id: string;
name: string;
role: string;
}
const CONTACTS: Contact[] = [
{ id: "c1", name: "Alex Rivera", role: "Engineering" },
{ id: "c2", name: "Jordan Lee", role: "Design" },
{ id: "c3", name: "Sam Patel", role: "Product" },
{ id: "c4", name: "Casey Kim", role: "Support" },
];
const ROW_HEIGHT = 64;
const ContactRow = memo(function ContactRow({
contact,
selected,
onPress,
}: {
contact: Contact;
selected: boolean;
onPress: (id: string) => void;
}) {
return (
<Pressable
onPress={() => onPress(contact.id)}
style={[styles.row, selected && styles.rowSelected]}
>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{contact.name[0]}</Text>
</View>
<View style={styles.meta}>
<Text style={styles.name} numberOfLines={1}>
{contact.name}
</Text>
<Text style={styles.role} numberOfLines={1}>
{contact.role}
</Text>
</View>
</Pressable>
);
});
function Separator() {
return <View style={styles.separator} />;
}
function EmptyState() {
return (
<View style={styles.empty}>
<Text style={styles.emptyTitle}>Sin contactos</Text>
<Text style={styles.emptyBody}>Prueba una búsqueda diferente o invita a compañeros de equipo.</Text>
</View>
);
}
export default function ContactPickerScreen() {
const [query, setQuery] = useState("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return CONTACTS;
return CONTACTS.filter(
(c) => c.name.toLowerCase().includes(q) || c.role.toLowerCase().includes(q),
);
}, [query]);
const handlePress = useCallback((id: string) => {
setSelectedId((prev) => (prev === id ? null : id));
}, []);
const renderItem: ListRenderItem<Contact> = useCallback(
({ item }) => (
<ContactRow contact={item} selected={item.id === selectedId} onPress={handlePress} />
),
[handlePress, selectedId],
);
return (
<View style={styles.screen}>
<View style={styles.header}>
<Text style={styles.heading}>Contactos</Text>
<Pressable
onPress={() => setQuery((q) => (q ? "" : "design"))}
style={styles.filterChip}
>
<Text style={styles.filterChipText}>{query ? "Limpiar filtro" : "Filtro: Design"}</Text>
</Pressable>
</View>
<FlatList
data={filtered}
keyExtractor={(item) => item.id}
renderItem={renderItem}
extraData={selectedId}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
ItemSeparatorComponent={Separator}
ListEmptyComponent={EmptyState}
contentContainerStyle={
filtered.length === 0 ? styles.emptyContainer : styles.list
}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#f8fafc" },
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 12,
gap: 12,
},
heading: { fontSize: 22, fontWeight: "700", color: "#0f172a" },
filterChip: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: "#e2e8f0",
},
filterChipText: { fontSize: 13, fontWeight: "600", color: "#334155" },
list: { paddingHorizontal: 16, paddingBottom: 24 },
row: {
height: ROW_HEIGHT,
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingHorizontal: 12,
borderRadius: 12,
backgroundColor: "#fff",
},
rowSelected: { backgroundColor: "#dbeafe" },
avatar: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#2563eb",
alignItems: "center",
justifyContent: "center",
},
avatarText: { color: "#fff", fontWeight: "700" },
meta: { flex: 1 },
name: { fontSize: 16, fontWeight: "600", color: "#0f172a" },
role: { fontSize: 13, color: "#64748b" },
separator: { height: 8 },
emptyContainer: { flexGrow: 1, justifyContent: "center" },
empty: { alignItems: "center", padding: 32, gap: 8 },
emptyTitle: { fontSize: 18, fontWeight: "700", color: "#0f172a" },
emptyBody: { fontSize: 14, color: "#64748b", textAlign: "center" },
});Lo que esto demuestra:
keyExtractor devuelve cadenas id estables - seguro cuando la lista se filtra o se reordena.getItemLayout con un ROW_HEIGHT fijo permite scrollToIndex sin medir.ItemSeparatorComponent como una función nombrada evita recrear el componente en cada renderizado.ListEmptyComponent se muestra cuando data está vacío después de filtrar - distinto de un spinner de carga.extraData={selectedId} fuerza actualizaciones de filas cuando cambia la selección pero la referencia de data no cambia.contentContainerStyle con flexGrow: 1 centra el estado vacío verticalmente.FlatList es un ScrollView virtualizado - monta solo las filas cerca de la ventana gráfica más una ventana configurable.data es el array de origen; renderItem recibe { item, index, separators } para cada fila visible.keyExtractor proporciona claves de React para las vistas de filas recicladas - crítico cuando se insertan, eliminan o reordenan elementos.getItemLayout precomputa (length, offset, index) para que la lista pueda saltar a cualquier índice sin medir el layout.ListEmptyComponent, ListHeaderComponent, y ListFooterComponent se renderizarán fuera del bucle de filas virtualizadas.FlatList extiende VirtualizedList, que agrupa el layout y difiere el trabajo fuera de pantalla para mantener el scroll a 60 fps.| Prop | Propósito | Cuándo es requerido |
|---|---|---|
data | Array de origen | Siempre |
renderItem | Renderizador de filas | Siempre |
keyExtractor | Clave de cadena estable por elemento | Fuertemente recomendado |
getItemLayout | Tamaño de fila fijo + offset | Altura de fila uniforme; scrollToIndex |
ItemSeparatorComponent | Divisor entre filas | Espaciado visual sin margen de fila |
ListEmptyComponent | UI de cero elementos | Cualquier lista que pueda estar vacía |
ListHeaderComponent / ListFooterComponent | Chrome no fila | Barras de búsqueda, pies de carga más |
extraData | Estado externo que afecta las filas | Selección, IDs expandidos, tema |
contentContainerStyle | Relleno interior del scroll / flex | Centrado de estado vacío, inserción inferior |
initialNumToRender | Tamaño de lote de primera pintura | Ajusta costo de primer fotograma vs flash en blanco |
// Divisor hairline - ancho completo
function HairlineSeparator() {
return <View style={{ height: StyleSheet.hairlineWidth, backgroundColor: "#e2e8f0" }} />;
}
// Divisor insertado - comienza después de la columna avatar
function InsetSeparator() {
return <View style={{ paddingLeft: 64 }}>
<View style={{ height: StyleSheet.hairlineWidth, backgroundColor: "#e2e8f0" }} />
</View>;
}
// Solo espaciado - sin línea visible
function GapSeparator() {
return <View style={{ height: 8 }} />;
}ItemSeparatorComponent sobre marginBottom en filas - los separadores no se reciclan con las vistas de filas y permanecen visualmente consistentes.ItemSeparatorComponent usando leadingItem / trailingItem de los helpers separators de renderItem.| Estado | data | Qué renderizar |
|---|---|---|
| Cargando (primera búsqueda) | [] o omitir lista | ActivityIndicator de pantalla completa encima de la lista |
| Vacío (éxito, cero filas) | [] | ListEmptyComponent |
| Error | [] | ListEmptyComponent con acción de reintento, o una vista de error dedicada |
| Cargado | [...items] | Filas normales |
ListEmptyComponent para el spinner de carga inicial - parpadea durante un fotograma cuando data comienza vacío.if (isLoading) return <Loader />; luego renderiza FlatList con ListEmptyComponent para el caso vacío establecido.// Altura fija - multiplica el índice por una constante
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
// Altura fija + separador fijo
const SEPARATOR = 8;
getItemLayout={(_, index) => ({
length: ROW_HEIGHT + SEPARATOR,
offset: (ROW_HEIGHT + SEPARATOR) * index,
index,
})}offset causa jank de scroll y objetivos scrollToIndex incorrectos.getItemLayout - valores incorrectos son peores que no tener valores.ROW_HEIGHT sin actualizar getItemLayout rompe las matemáticas de la posición de scroll.import type { FlatListProps, ListRenderItem } from "react-native";
interface Place {
id: string;
name: string;
}
type PlaceListProps = Omit<FlatListProps<Place>, "data" | "renderItem" | "keyExtractor"> & {
places: Place[];
onSelect: (place: Place) => void;
};
function PlaceList({ places, onSelect, ...rest }: PlaceListProps) {
const renderItem: ListRenderItem<Place> = useCallback(
({ item }) => <PlaceRow place={item} onPress={() => onSelect(item)} />,
[onSelect],
);
return (
<FlatList
data={places}
keyExtractor={(item) => item.id}
renderItem={renderItem}
{...rest}
/>
);
}FlatList<Place> para que renderItem y keyExtractor infieran el tipo de item.ListRenderItem<T> para escritura estable de useCallback.ListEmptyComponent acepta React.ComponentType o React.ReactElement - prefiere una referencia de componente sobre () => <View /> en línea.| Parámetro | Tipo | Descripción |
|---|---|---|
data | readonly T[] | null | undefined | Elementos a virtualizar; null/undefined se tratan como vacíos |
renderItem | ListRenderItem<T> | Devuelve el elemento de fila para un elemento |
keyExtractor | (item: T, index: number) => string | Clave única estable; evita el índice para listas mutables |
getItemLayout | (data, index) => { length, offset, index } | Layout precomputado para filas de tamaño fijo |
extraData | any | Desencadena actualización de filas cuando cambia el estado externo |
ItemSeparatorComponent | ComponentType | Renderizado entre elementos, no después del último |
ListEmptyComponent | ComponentType | ReactElement | Se muestra cuando la longitud de data es 0 |
Usar el índice de array en keyExtractor - Insertar o eliminar filas re-codifica hermanos, causando desbordamiento de estado en vistas recicladas (avatar incorrecto, checkbox obsoleto). Solución: Usa un id estable asignado por servidor o cliente.
Flecha renderItem en línea en cada renderizado - Una nueva identidad de función anula memo en componentes de fila. Solución: useCallback el renderizador o extrae un ListRenderItem con deps estables.
Olvidar extraData para estado de selección - FlatList es un PureComponent; data sin cambios salta renderItem incluso cuando selectedId cambia. Solución: Pasa extraData={selectedId} (o un array/hash del estado que afecta a las filas).
Matemáticas incorrectas de getItemLayout con separadores - La altura del separador debe incluirse en length y offset o scrollToIndex aterriza entre filas. Solución: Agrega altura del separador a la fórmula u omite getItemLayout para layouts variables.
ListEmptyComponent durante carga inicial - Los usuarios ven "Sin resultados" durante un fotograma antes de que lleguen los datos. Solución: Muestra una rama de carga fuera de la lista; reserva ListEmptyComponent para resultados vacíos establecidos.
contentContainerStyle sin flexGrow: 1 para estados vacíos centrados - UI vacío se adhiere a la parte superior del área de scroll. Solución: contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }} cuando data.length === 0.
ItemSeparatorComponent anónimo (() => ...) - Crea un nuevo tipo de componente en cada renderizado, forzando remontajes de separador innecesarios. Solución: Eleva a una función nombrada o envuelve con memo.
| Alternativa | Usarlo cuando | No usarlo cuando |
|---|---|---|
FlatList | Listas largas homogéneas, APIs RN estándar | Feeds de mayor rendimiento - considera FlashList |
SectionList | Datos agrupados en secciones tituladas | Lista plana sin encabezados de sección |
ScrollView + map | < ~20 filas estáticas, contenido mixto | Cientos de filas - sin virtualización |
FlashList | Feeds críticas de rendimiento, alturas de fila variable con estimaciones | Pantallas de configuración pequeñas donde FlatList es suficiente |
FlashList estimatedItemSize | Migración desde FlatList para rendimiento | Necesitas cero dependencias nuevas |
No estrictamente - FlatList vuelve a key={index}. Para cualquier lista que muta (añadir, eliminar, reordenar, filtrar), las claves estables son obligatorias. Siempre proporciona keyExtractor={(item) => item.id} cuando los elementos tienen ids.
Cuando cada fila (más separador) tiene una altura fija conocida y necesitas scrollToIndex, scrollToItem, o initialScrollIndex sin retrasos de medición de layout. Omítelo cuando las filas envuelven texto o tienen alturas de media dinámica.
FlatList compara data superficialmente. Si la apariencia de la fila depende del estado fuera de data - selección, filas expandidas, modo oscuro - pasa ese estado como extraData para que las filas visibles se re-renderizen cuando cambia.
Usa ListHeaderComponent para contenido que se desplaza con la lista (campo de búsqueda, título de sección). Fija un encabezado fuera de FlatList cuando debe permanecer fijo mientras se desplazan las filas.
Los separadores no están vinculados a las vistas de filas recicladas y mantienen el espaciado consistente cuando la virtualización remonta celdas. El margen en la última fila a menudo necesita hacks :last-child - los separadores manejan las brechas entre filas limpiamente.
contentContainerStyle={
data.length === 0
? { flexGrow: 1, justifyContent: "center", padding: 32 }
: { padding: 16 }
}flexGrow: 1 permite que el contenedor de contenido llene la ventana gráfica de scroll.
Sí - devuelve un Pressable con onPress que desencadena refetch o navegación. Mantén acciones de estado vacío en ListEmptyComponent; mantén spinners de carga fuera de la lista.
Causas comunes: falta getItemLayout en filas de altura variable, índice fuera de rango, o lista no aún diseñada. Llama scrollToIndex en onLayout o requestAnimationFrame después de establecer datos; proporciona getItemLayout correcto para alturas fijas.
Extrae a useCallback o una constante ListRenderItem cuando las filas usan memo. El ({ item }) => <Row item={item} /> en línea está bien para prototipos; los feeds de producción deben estabilizar la referencia.
Usa FlatList<MyItem> y ListRenderItem<MyItem>. TypeScript infiere item en ambas devoluciones de llamada. Evita convertir item as MyItem dentro de la lista.
Sí - la lista debe tener altura acotada. Pon FlatList en una View con flex: 1 (o style={{ flex: 1 }} en la lista). Los padres de altura ilimitada rompen la virtualización.
Recuento de filas renderizadas en el primer montaje (predeterminado ~10). Redúcelo para filas pesadas para mejorar el tiempo-a-interactivo; auméntalo para reducir el área en blanco en pantallas altas. Ajusta con maxToRenderPerBatch y windowSize para sentir el scroll.
Pasa refreshing y onRefresh a FlatList (o un prop refreshControl). Consulta Infinite Scroll & Pagination para patrones de paginación y actualización juntos.
Bajo ~20 filas estáticas simples, ScrollView + map está bien. Más allá de eso, o cuando las filas son costosas, FlatList virtualiza y mantiene la memoria plana. Consulta Lists Basics para el umbral de decisión.
Después de dominar estos props, vete a List Performance Tuning para ajuste de memo, windowSize, y removeClippedSubviews.
ScrollView es suficiente y cuándo la virtualización es obligatoriaonEndReached, pull-to-refresh, y pies de cargamemo, windowSize, y renderItem estableextraData y useCallback para estabilidad de filasVersiones 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: 16 jul 2026