Receitas de FlatList
keyExtractor, getItemLayout, separadores e estados vazios - as props que você usa em todas as telas de feed.
Busque em todas as páginas da documentação
keyExtractor, getItemLayout, separadores e estados vazios - as props que você usa em todas as telas de feed.
Cartão de receita de referência rápida - pronto para copiar e colar.
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}>Nenhuma mensagem ainda</Text>
<Text style={styles.emptyBody}>Puxe para atualizar ou inicie uma conversa.</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" },
});Quando usar isso: Qualquer lista homogênea com rolagem vertical - linhas de caixa de entrada, itens de configurações, cartões de produtos com 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}>Nenhum contato</Text>
<Text style={styles.emptyBody}>Tente uma busca diferente ou convide colegas.</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}>Contatos</Text>
<Pressable
onPress={() => setQuery((q) => (q ? "" : "design"))}
style={styles.filterChip}
>
<Text style={styles.filterChipText}>{query ? "Limpar filtro" : "Filtrar: 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" },
});O que isso demonstra:
keyExtractor retorna strings id estáveis - seguro quando a lista é filtrada ou reordenada.getItemLayout com um ROW_HEIGHT fixo permite scrollToIndex sem medição.ItemSeparatorComponent como uma função nomeada evita recriar o componente a cada renderização.ListEmptyComponent é exibido quando data está vazio após a filtragem - distinto de um spinner de carregamento.extraData={selectedId} força atualizações de linha quando a seleção muda, mas a referência data não muda.contentContainerStyle com flexGrow: 1 centraliza o estado vazio verticalmente.FlatList é uma ScrollView virtualizada - ela monta apenas as linhas próximas à viewport mais uma janela configurável.data é o array de origem; renderItem recebe { item, index, separators } para cada linha visível.keyExtractor fornece chaves React para visualizações de linha recicladas - crítico quando itens são inseridos, excluídos ou reordenados.getItemLayout pré-computa (length, offset, index) para que a lista possa pular para qualquer índice sem medição de layout.ListEmptyComponent, ListHeaderComponent e ListFooterComponent renderizam fora do loop de linhas virtualizadas.FlatList estende VirtualizedList, que agrupa layouts e adia o trabalho fora da tela para manter a rolagem a 60 fps.| Prop | Propósito | Quando necessário |
|---|---|---|
data | Array de origem | Sempre |
renderItem | Renderizador de linha | Sempre |
keyExtractor | Chave string estável por item | Altamente recomendado |
getItemLayout | Tamanho fixo da linha + offset | Altura de linha uniforme; scrollToIndex |
ItemSeparatorComponent | Divisor entre linhas | Espaçamento visual sem margem de linha |
ListEmptyComponent | UI para zero itens | Qualquer lista que possa estar vazia |
ListHeaderComponent / ListFooterComponent | Elementos visuais não-linha | Barras de pesquisa, rodapés de "carregar mais" |
extraData | Estado externo que afeta as linhas | Seleção, IDs expandidos, tema |
contentContainerStyle | Preenchimento interno de rolagem / flex | Centralização de estado vazio, margem inferior |
initialNumToRender | Tamanho do primeiro lote de pintura | Ajuste o custo do primeiro frame vs. flash em branco |
// Divisor de linha fina - largura total
function HairlineSeparator() {
return <View style={{ height: StyleSheet.hairlineWidth, backgroundColor: "#e2e8f0" }} />;
}
// Divisor com recuo - começa após a coluna do avatar
function InsetSeparator() {
return <View style={{ paddingLeft: 64 }}>
<View style={{ height: StyleSheet.hairlineWidth, backgroundColor: "#e2e8f0" }} />
</View>;
}
// Apenas espaçamento - sem linha visível
function GapSeparator() {
return <View style={{ height: 8 }} />;
}ItemSeparatorComponent em vez de marginBottom nas linhas - os separadores não são reciclados com as visualizações de linha e permanecem visualmente consistentes.ItemSeparatorComponent usando leadingItem / trailingItem dos helpers separators de renderItem.| Estado | data | O que renderizar |
|---|---|---|
| Carregando (primeira busca) | [] ou omitir lista | ActivityIndicator em tela cheia acima da lista |
| Vazio (sucesso, zero linhas) | [] | ListEmptyComponent |
| Erro | [] | ListEmptyComponent com ação de tentar novamente, ou uma view de erro dedicada |
| Carregado | [...itens] | Linhas normais |
ListEmptyComponent para o spinner de carregamento inicial - ele pisca por um frame quando data começa vazio.if (isLoading) return <Loader />; em seguida, renderize FlatList com ListEmptyComponent para o caso vazio estabelecido.getItemLayout// Altura fixa - multiplique o índice pela constante
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
// Altura fixa + separador fixo
const SEPARATOR = 8;
getItemLayout={(_, index) => ({
length: ROW_HEIGHT + SEPARATOR,
offset: (ROW_HEIGHT + SEPARATOR) * index,
index,
})}offset causam travamentos na rolagem e alvos incorretos para scrollToIndex.getItemLayout - valores incorretos são piores do que nenhum valor.ROW_HEIGHT sem atualizar getItemLayout quebra a matemática da posição de rolagem.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 e keyExtractor infiram o tipo item.ListRenderItem<T> para tipagem useCallback estável.ListEmptyComponent aceita React.ComponentType ou React.ReactElement - prefira uma referência de componente em vez de () => <View /> inline.| Parâmetro | Tipo | Descrição |
|---|---|---|
data | readonly T[] | null | undefined | Itens a serem virtualizados; null/undefined tratados como vazios |
renderItem | ListRenderItem<T> | Retorna o elemento da linha para um item |
keyExtractor | (item: T, index: number) => string | Chave estável única; evite índice para listas mutáveis |
getItemLayout | (data, index) => { length, offset, index } | Layout pré-computado para linhas de tamanho fixo |
extraData | any | Dispara a atualização da linha quando o estado externo muda |
ItemSeparatorComponent | ComponentType | Renderizado entre os itens, não após o último |
ListEmptyComponent | ComponentType | ReactElement | Exibido quando o comprimento de data é 0 |
Usando o índice do array em keyExtractor - Inserir ou excluir linhas re-chaveia os irmãos, causando vazamento de estado em visualizações recicladas (avatar incorreto, checkbox desatualizado). Correção: Use um id estável atribuído pelo servidor ou cliente.
Seta renderItem inline em cada renderização - Uma nova identidade de função anula memo nos componentes de linha. Correção: Use useCallback no renderizador ou extraia um ListRenderItem com dependências estáveis.
Esquecendo extraData para o estado de seleção - FlatList é um PureComponent; data inalterado pula renderItem mesmo quando selectedId muda. Correção: Passe extraData={selectedId} (ou um array/hash de estado que afeta a linha).
Matemática incorreta de getItemLayout com separadores - A altura do separador deve ser incluída em length e offset, ou scrollToIndex cairá entre as linhas. Correção: Adicione a altura do separador à fórmula ou omita getItemLayout para layouts variáveis.
ListEmptyComponent durante o carregamento inicial - Os usuários veem "Nenhum resultado" por um frame antes que os dados cheguem. Correção: Mostre um branch de carregamento fora da lista; reserve ListEmptyComponent para resultados vazios estabelecidos.
contentContainerStyle sem flexGrow: 1 para estados vazios centralizados - A UI vazia fica presa no topo da área de rolagem. Correção: contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }} quando data.length === 0.
ItemSeparatorComponent anônimo () => ... - Cria um novo tipo de componente a cada renderização, forçando remontagens desnecessárias do separador. Correção: Eleve para uma função nomeada ou um wrapper memo.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
FlatList | Listas homogêneas longas, APIs RN padrão | Feeds de maior taxa de transferência - considere FlashList |
SectionList | Dados agrupados em seções com título | Lista plana sem cabeçalhos de seção |
ScrollView + map | < ~20 linhas estáticas, conteúdo misto | Centenas de linhas - sem virtualização |
FlashList | Feeds críticos de desempenho, alturas de linha variáveis com estimativas | Telas de configuração minúsculas onde FlatList é suficiente |
FlashList estimatedItemSize | Migrando de FlatList para desempenho | Você precisa de zero dependências novas |
Não estritamente - FlatList usa key={index} como fallback. Para qualquer lista que muta (adiciona, remove, reordena, filtra), chaves estáveis são obrigatórias. Sempre forneça keyExtractor={(item) => item.id} quando os itens tiverem ids.
Quando cada linha (mais o separador) tem uma altura fixa conhecida e você precisa de scrollToIndex, scrollToItem ou initialScrollIndex sem atrasos de medição de layout. Omita-o quando as linhas quebram o texto ou têm alturas de mídia dinâmicas.
FlatList compara superficialmente data. Se a aparência da linha depende de estado fora de data - seleção, linhas expandidas, modo escuro - passe esse estado como extraData para que as linhas visíveis re-renderizem quando ele mudar.
Use ListHeaderComponent para conteúdo que rola com a lista (campo de pesquisa, título da seção). Fixe um cabeçalho fora do FlatList quando ele precisar permanecer fixo enquanto as linhas rolam.
Os separadores não estão vinculados às visualizações de linha recicladas e mantêm o espaçamento consistente quando a virtualização remonta as células. A margem na última linha geralmente requer hacks :last-child - os separadores lidam com lacunas entre linhas de forma limpa.
contentContainerStyle={
data.length === 0
? { flexGrow: 1, justifyContent: "center", padding: 32 }
: { padding: 16 }
}flexGrow: 1 permite que o contêiner de conteúdo preencha o viewport de rolagem.
Sim - retorne um Pressable com onPress que aciona a re-busca ou navegação. Mantenha as ações de estado vazio em ListEmptyComponent; mantenha os spinners de carregamento fora da lista.
Causas comuns: getItemLayout ausente em linhas de altura variável, índice fora do intervalo ou lista ainda não renderizada. Chame scrollToIndex em onLayout ou requestAnimationFrame após os dados serem definidos; forneça getItemLayout correto para alturas fixas.
Extraia para useCallback ou uma constante ListRenderItem quando as linhas usam memo. Inline ({ item }) => <Row item={item} /> é bom para protótipos; feeds de produção devem estabilizar a referência.
Use FlatList<MyItem> e ListRenderItem<MyItem>. O TypeScript infere item em ambos os callbacks. Evite lançar item as MyItem dentro da lista.
Sim - a lista deve ter altura limitada. Coloque FlatList em uma View com flex: 1 (ou style={{ flex: 1 }} na lista). Pais com altura ilimitada quebram a virtualização.
Contagem de linhas renderizadas na primeira montagem (padrão ~10). Diminua-o para linhas pesadas para melhorar o tempo de interatividade; aumente-o para reduzir a área em branco em telas altas. Ajuste com maxToRenderPerBatch e windowSize para a sensação de rolagem.
Passe refreshing e onRefresh para FlatList (ou uma prop refreshControl). Veja Scroll Infinito e Paginação para padrões de paginação e atualização juntos.
Menos de ~20 linhas estáticas simples, ScrollView + map é bom. Além disso, ou quando as linhas são caras, FlatList virtualiza e mantém a memória plana. Veja Noções Básicas de Listas para o limite de decisão.
Após dominar essas props, passe para Otimização de Desempenho de Lista para ajuste de memo, windowSize e renderItem estável.
ScrollView é suficiente e quando a virtualização é obrigatóriaonEndReached, pull-to-refresh e rodapés de carregamentomemo, windowSize e renderItem estávelextraData e useCallback para estabilidade de linhaVersões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026