Dia 5 — Listas: FlatList e SectionList
FlatList e o porquê de não usar map
FlatList e o porquê de não usar map
map num View desenha todos os itens, um por um, e o React Native monta
um componente nativo para cada um. Com trinta itens isso é invisível; com três
mil, são três mil caixas nativas criadas no mesmo quadro, a memória sobe e o app
começa a arrastar na rolagem. A causa não é o map: é que todos os itens
existem ao mesmo tempo.
FlatList resolve isso com virtualização: ela desenha só o que está
visível mais uma margem, e vai descartando o que saiu da tela. Do ponto de vista
do aluno, FlatList parece uma lista normal — o map sumiu de vista — mas por
trás existem duas propriedades que precisam estar presentes para a
virtualização funcionar:
| Propriedade | O que faz |
|---|---|
data | o array que alimenta a lista |
renderItem | a função que devolve o componente de cada item |
keyExtractor | a função que dá a chave única de cada item |
O exemplo desta página põe as duas formas lado a lado e o número é o mesmo: 40
componentes criados no map e 40 na lista. A contagem inicial é igual, e é
justamente esse número que engana: o que difere não é o que foi criado, é o que
ficou na memória depois. A pergunta certa não é "quantos componentes a lista
criou" e sim "quantos continuam vivos enquanto o usuário rola".
O exemplo responde à segunda pergunta com a conta da janela: item de altura 44
em tela de 700 dá 16 itens visíveis. Com map os 40 continuam na memória;
com lista sobram 17 — os 16 da tela mais um de margem. Os outros 23 foram
descartados e serão criados de novo quando voltarem. É esse descarte, e não
economia de tempo de criação, que mantém o app rolando liso.
keyExtractor não é decoração. Sem ele, o React Native cai no index da
posição, e a lista quebra no lugar errado quando um item é removido do começo:
a posição 3 deixa de ser a posição 3, o componente reaproveita o estado do item
errado, e o sintoma é um checkbox marcado no item vizinho. A chave tem que
viver no dado — o id do registro — e a função é só a forma de chegar nela.
<FlatList data={pedidos} keyExtractor={(item) => item.id} renderItem={({ item }) => <ItemPedido pedido={item} />} />
O exemplo grava as duas funções lado a lado para a diferença ficar visível: com
o índice, o quarto pedido recebe a chave 3; com o id, recebe p4. São o
mesmo item e dois identificadores, e a chave por posição descreve onde o
item estava, enquanto a chave por id descreve qual item é. Quando um item
sai do começo da lista, a posição 3 passa a ser outro pedido — e o estado que o
React guardou para a chave 3 continua lá, no lugar errado.
renderItem recebe três coisas
O objeto que renderItem recebe tem item, index e separators. Usar
index como chave é justamente o erro acima; usá-lo para exibir "item 1 de 20"
também, porque em lista paginada o número muda conforme o carregamento. index
serve para o que realmente é posicional, que é raro.
A função é chamada uma vez por item visível, e é o lugar certo para a lógica
de apresentação do item: formatar a data, escolher o ícone pelo status, cortar
o texto. Deixar essa lógica dentro do renderItem e não no componente do item
significa que o componente do item continua testável e reaproveitável.
O item é o registro inteiro, com todos os campos — e é por isso que o
renderItem do exemplo desestrutura só o que precisa: ({ item }). Trazer o
index junto sem usar é a forma mais comum de deixar o dado disponível para o
uso errado mais tarde. separators é o terceiro membro do objeto: é o que
permite ao item saber que está no topo ou no fim de uma seção, e quem sabe
disso é o próprio item, não o renderItem.
A consequência de virtualização é que o renderItem roda mais de uma vez para o
mesmo item. Ele é criado, descartado ao sair da tela e criado de novo quando
volta. Nada pode ser guardado em variable de fora do componente do item, porque
o componente é destruído e recriado — quem guarda estado entre as duas vezes é o
id, que é a chave.
quando o ScrollView ainda serve
ScrollView com map dentro é aceitável quando a lista é curta e **não
cresce**: os dias da semana, as três opções de um seletor, os itens de um menu.
O critério é se o conteúdo vem de uma requisição: se a lista pode ter mil
itens, é FlatList desde o começo. Trocar depois dá trabalho, porque a
diferença não fica só na tag — muda a forma de carregar mais item, de
medir posição e de atualizar um item específico.
A propriedade que controla a margem é windowSize, e ela é o ajuste fino da
virtualização: ela diz quantas vezes a altura da tela vale a área montada. O
padrão renderiza algumas telas além do visível, o que dá rolagem mais estável ao
pular rápido. Baixar o valor economiza memória e aumenta o trabalho ao voltar
para cima, que é a troca real.
Para o comportamento de lista rolável dentro de coluna rolável, há um detalhe
conhecido: o FlatList em ScrollView com o mesmo eixo perde a virtualização,
porque o sistema não consegue saber a altura do conteúdo. A solução é
inverter: a lista na horizontal dentro de uma coluna vertical, que funciona
porque cada eixo tem a sua rolagem.
O que dá para reduzir na primeira montagem é initialNumToRender: quantos itens
são desenhados antes da lista ter a chance de medir. Valor baixo faz a abertura
mais rápida e empurra o resto para o primeiro quadro de rolagem; valor alto faz o
oposto. Nenhum dos dois valores conserta lista que cresce — os dois só ajustam
quando o custo aparece.
Exemplo
// `map` dentro de `View` cria um componente por item; e a lista com // virtualizacao que desenha so o que esta visivel. O exemplo monta as // duas arvores e compara quantos componentes cada uma produziu. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ item: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#e6e8eb' }, container: { flex: 1 }, }); // O array que alimenta a lista. const pedidos = Array.from({ length: 40 }, (_, i) => ({ id: 'p' + (i + 1), cliente: 'cliente ' + (i + 1), total: 10 + i * 3, })); // (1) o jeito que trava: `map` monta um componente para CADA item. function ComMap() { return ( <View style={estilos.container}> {pedidos.map((pedido) => ( <Text key={pedido.id} style={estilos.item}> {pedido.cliente} - {pedido.total} </Text> ))} </View> ); } // (2) o jeito da lista com virtualizacao: ela recebe `data` e chama // `renderItem` so para o que cabe na tela. O exemplo desenha com `map` // porque o componente de lista nao esta no ambiente de execucao; a // logica do `renderItem` e a mesma. function ComLista(props) { return ( <View style={estilos.container}> {props.data.map((item) => props.renderItem({ item, index: -1 }))} </View> ); } const arvoreMap = ComMap(); const filhosMap = arvoreMap.props.children; console.log('com `map`, componentes criados:', filhosMap.length); console.log('o ultimo item criado:', filhosMap[filhosMap.length - 1].props.children); // `renderItem` e uma funcao: o componente do item e o que ela devolve. const ItemPedido = ({ pedido }) => ( <Text style={estilos.item}>{pedido.cliente}</Text> ); const arvoreLista = ComLista({ data: pedidos, renderItem: ({ item }) => <ItemPedido pedido={item} /> }); console.log('com lista, componentes criados:', arvoreLista.props.children.length); console.log('o mesmo total: a diferenca esta no que fica na memoria depois'); // A janela: e isso que a virtualizacao controla. const alturaDoItem = 44; const alturaDaTela = 700; const janela = Math.ceil(alturaDaTela / alturaDoItem); console.log('altura do item:', alturaDoItem); console.log('itens visiveis:', janela); console.log('com `map` existem', pedidos.length, 'na memoria; com lista,', janela + 1); // `keyExtractor` sem `id` cai no indice, e o item reaproveita o estado errado. function chavePorIndice(item, indice) { return String(indice); } function chavePorId(item) { return String(item.id); } console.log('chave por indice:', chavePorIndice(pedidos[3], 3), '| chave por id:', chavePorId(pedidos[3])); console.log('o item da posicao 3 muda de id quando algo e removido do inicio');
Saída real
com `map`, componentes criados: 40 o ultimo item criado: [ 'cliente 40', ' - ', 127 ] com lista, componentes criados: 40 o mesmo total: a diferenca esta no que fica na memoria depois altura do item: 44 itens visiveis: 16 com `map` existem 40 na memoria; com lista, 17 chave por indice: 3 | chave por id: p4 o item da posicao 3 muda de id quando algo e removido do inicio
SectionList, itemSeparator e lista vazia
SectionList, itemSeparator e lista vazia
SectionList é o FlatList para dados agrupados: em vez de um array de
itens, recebe sections, e cada seção é um objeto com title, data e uma
key. É a estrutura que resolve lista de contatos por letra, menu por
categoria, e histórico de pedido por mês. O agrupamento já vem pronto na
propriedade data da seção — o componente não agrupa nada, ele exibe a
divisão que a lista recebeu.
O exemplo desta página monta as duas seções e imprime o que cada propriedade
entrega: duas seções no total, a de maio com 2 itens e a de junho com 1, e
as chaves como [ '2026-05', '2026-06' ]. A chave de cada seção é obrigatória
pelo mesmo motivo da chave de cada item — sem ela, o agrupamento volta a ser
identificado por posição, e remover um mês do topo reidentifica todos os outros.
A estrutura de cada seção é o que o renderSectionHeader recebe: o objeto
inteiro. O exemplo passa { section: secao } e o cabeçalho lê section.title,
section.data.length e section.total — três campos, e total só existe
porque a seção o carrega. Nenhum campo do cabeçalho é calculado pelo componente:
tudo o que ele mostra saiu do objeto.
<SectionList sections={secoes} keyExtractor={(item) => item.id} renderItem={({ item }) => <Linha item={item} />} renderSectionHeader={({ section }) => <Titulo titulo={section.title} />} />
As duas propriedades que fazem a seção virar cabeçalho visual são
renderSectionHeader, que desenha o topo de cada seção, e
sectionStyle, que dá o estilo do container da seção. A primeira é função e
recebe { section } — o mesmo objeto de dados que veio em sections, então
dá para ler section.title, section.total ou qualquer campo que a seção
carregue.
o separador é componente, não estilo
itemSeparatorComponent não recebe um estilo: recebe um componente. É essa
a pegadinha, porque columnWrapperStyle e ListHeaderComponent recebem
estilo e componente em posicionamentos parecidos da documentação. A
consequência é que um separador que muda com a seção — linha em cima da
primeira, nenhuma depois da última — precisa de uma função que decide o que
devolver, e o highlighted que o separador recebe indica se está entre dois
itens ou no fim da seção.
O exemplo implementa exatamente essa função, e o par de console é a prova de
que a propriedade decide: com highlighted verdadeiro, o separador devolve um
View; com highlighted falso, devolve null e nada é desenhado. O mesmo
componente produz as duas coisas, e quem escolhe é a posição — não o estilo, e
não a seção.
A consequência prática é que separador que respeita a borda da seção se escreve
com uma condição dentro do componente, não com uma propriedade nova. null é o
"não desenha": não ocupa espaço, não aparece e não empurra o item seguinte.
ListEmptyComponent cobre o caso mais comum de erro percebido: a lista
carregou, veio vazia, e a tela mostra um retângulo branco sem explicação nenhuma.
O componente recebe a lista vazia como filho e devolve o que deve aparecer
nele — "nenhum registro ainda", com um botão de criar. Ele só aparece quando
data ou sections estão vazios, e é a diferença entre uma tela que parece
quebrada e uma tela que informa.
O exemplo testa os dois lados da condição, e o teste é feito com some sobre as
seções: com pelo menos uma seção com item, o resultado é true e o vazio não
aparece. Sem seções, a árvore devolvida não tem nenhum filho — 0 — que é
exatamente o estado em que o ListEmptyComponent assume. O componente de vazio
devolve Text com o texto "nenhum registro ainda", e o texto é lido em
props.children.
O erro de meio termo é o mais comum: mostrar o vazio e a lista ao mesmo
tempo. Isso acontece quando o estado inicial do aplicativo é "vazio" e o
carregamento ainda não começou — a tela mostra "nenhum registro" para quem
espera. A distinção que resolve são três estados, não dois: carregando, vazio
de verdade e com dados.
esticar o item em telas largas
columnWrapperStyle define a linha de colunas e é o que permite numColumns
maior que um: { flexDirection: 'row' } com flex: 1 no item distribui os
itens pela largura. numColumns é estático na lista, e mudar o valor depois
provoca aviso do sistema sobre a troca de coluna — o jeito certo é ler a
largura da janela e escolher o número antes de montar.
O exemplo mostra os dois estilos que fazem a grade funcionar e faz a conta da
altura: linha de colunas com { flexDirection: 'row' } e item com `{ flex:
1 }. Com flex: 1` no item, cada coluna divide a largura igualmente, e é por
isso que o columnWrapperStyle precisa vir com flexDirection: 'row' — sem ele,
a linha de colunas empilha os itens em vez de distribuí-los.
A conta fecha em Math.ceil de 4 por 2, que dá 2 linhas. O arredondamento
para cima é o detalhe: com 5 itens em 2 colunas, são 3 linhas e a última
fica com um item só. Uma grade que esconde a sobra deixa um item invisível, e
o sintoma é um item faltando sem nenhuma mensagem.
Exemplo
// A lista em secoes recebe `sections` (grupo com `title` e `data`), e o // separador e um COMPONENTE, nao um estilo. O exemplo monta as secoes e // mostra o que cada propriedade devolve. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ cabecalho: { fontSize: 13, fontWeight: '700', color: '#57606a', padding: 8, backgroundColor: '#f6f8fa' }, linha: { padding: 12, flexDirection: 'row', justifyContent: 'space-between' }, separador: { height: 1, backgroundColor: '#e6e8eb' }, vazio: { padding: 24, textAlign: 'center', color: '#8b949e' }, }); const secoes = [ { key: '2026-05', title: 'maio de 2026', total: 2, data: [ { id: 'p41', cliente: 'ana', total: 90 }, { id: 'p42', cliente: 'bruno', total: 45 }, ] }, { key: '2026-06', title: 'junho de 2026', total: 1, data: [ { id: 'p43', cliente: 'carla', total: 120 }, ] }, ]; // (1) `sections` e o agrupamento pronto; o componente nao agrupa nada. function ListaEmSecoes(props) { return ( <View> {props.sections.map((secao) => ( <View key={secao.key}> <Text style={estilos.cabecalho}>{secao.title}</Text> {secao.data.map((item) => ( <Text key={item.id} style={estilos.linha}>{item.cliente}</Text> ))} </View> ))} </View> ); } const arvore = ListaEmSecoes({ sections: secoes }); console.log('quantas secoes:', arvore.props.children.length); // (2) `renderSectionHeader` recebe o objeto da secao, inteiro. for (const secao of secoes) { const cabecalho = { section: secao }; console.log('cabecalho de', cabecalho.section.title, '| itens:', cabecalho.section.data.length, '| total:', cabecalho.section.total); } // (3) o total de itens e a soma do `data` de cada secao. const total = secoes.reduce((soma, secao) => soma + secao.data.length, 0); console.log('total de itens:', total); console.log('chave de cada secao:', secoes.map((s) => s.key)); // (4) `itemSeparatorComponent` e um COMPONENTE: recebe `highlighted` e // devolve o que vai ser desenhado entre dois itens. function Separador(props) { return props.highlighted ? <View style={estilos.separador} /> : null; } console.log('separador entre dois itens:', Separador({ highlighted: true }).type); console.log('separador no fim da secao:', Separador({ highlighted: false })); // (5) `ListEmptyComponent` so aparece com a lista vazia. function Vazio() { return <Text style={estilos.vazio}>nenhum registro ainda</Text>; } const temDados = secoes.some((secao) => secao.data.length > 0); console.log('a lista tem dados:', temDados, '| o vazio aparece:', !temDados); const arvoreVazia = ListaEmSecoes({ sections: [] }); console.log('sem secoes, a lista nao tem nenhum filho:', arvoreVazia.props.children.length); console.log('o componente de vazio devolve:', Vazio().type, Vazio().props.children); // (6) `columnWrapperStyle` e a linha das colunas, com `numColumns`. const linhaDeColunas = StyleSheet.flatten({ flexDirection: 'row' }); const item = StyleSheet.flatten({ flex: 1 }); console.log('linha de colunas:', linhaDeColunas, '| item:', item); console.log('4 itens em 2 colunas sao', Math.ceil(4 / 2), 'linhas');
Saída real
quantas secoes: 2
cabecalho de maio de 2026 | itens: 2 | total: 2
cabecalho de junho de 2026 | itens: 1 | total: 1
total de itens: 3
chave de cada secao: [ '2026-05', '2026-06' ]
separador entre dois itens: View
separador no fim da secao: null
a lista tem dados: true | o vazio aparece: false
sem secoes, a lista nao tem nenhum filho: 0
o componente de vazio devolve: Text nenhum registro ainda
linha de colunas: { flexDirection: 'row' } | item: { flex: 1 }
4 itens em 2 colunas sao 2 linhas